diff --git a/.gitattributes b/.gitattributes index 427cb28..2149ea1 100644 --- a/.gitattributes +++ b/.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/.github/actions/sanity_tests.sh b/.github/actions/sanity_tests.sh index 8984b26..13b5e39 100755 --- a/.github/actions/sanity_tests.sh +++ b/.github/actions/sanity_tests.sh @@ -1,10 +1,10 @@ set -e ./build/bin/tester/sameboy_tester --jobs 5 \ - --length 40 .github/actions/cgb_sound.gb \ + --length 45 .github/actions/cgb_sound.gb \ --length 10 .github/actions/cgb-acid2.gbc \ --length 10 .github/actions/dmg-acid2.gb \ ---dmg --length 40 .github/actions/dmg_sound-2.gb \ +--dmg --length 45 .github/actions/dmg_sound-2.gb \ --dmg --length 20 .github/actions/oam_bug-2.gb mv .github/actions/dmg{,-mode}-acid2.bmp @@ -15,18 +15,18 @@ mv .github/actions/dmg{,-mode}-acid2.bmp set +e FAILED_TESTS=` -shasum .github/actions/*.bmp | grep -q -E -v \(\ -44ce0c7d49254df0637849c9155080ac7dc3ef3d\ \ .github/actions/cgb-acid2.bmp\|\ +shasum .github/actions/*.bmp | grep -E -v \(\ +5283564df0cf5bb78a7a90aff026c1a4692fd39e\ \ .github/actions/cgb-acid2.bmp\|\ dbcc438dcea13b5d1b80c5cd06bda2592cc5d9e0\ \ .github/actions/cgb_sound.bmp\|\ 0caadf9634e40247ae9c15ff71992e8f77bbf89e\ \ .github/actions/dmg-acid2.bmp\|\ -c50daed36c57a8170ff362042694786676350997\ \ .github/actions/dmg-mode-acid2.bmp\|\ +a732077f98f43d9231453b1764d9f797a836924d\ \ .github/actions/dmg-mode-acid2.bmp\|\ c9e944b7e01078bdeba1819bc2fa9372b111f52d\ \ .github/actions/dmg_sound-2.bmp\|\ f0172cc91867d3343fbd113a2bb98100074be0de\ \ .github/actions/oam_bug-2.bmp\ \)` if [ -n "$FAILED_TESTS" ] ; then echo "Failed the following tests:" - echo $FAILED_TESTS | tr " " "\n" | grep -q -o -E "[^/]+\.bmp" | sed s/.bmp// | sort + echo $FAILED_TESTS | tr " " "\n" | grep -o -E "[^/]+\.bmp" | sed s/.bmp// | sort exit 1 fi diff --git a/.github/workflows/sanity.yml b/.github/workflows/sanity.yml index f460931..ac37323 100644 --- a/.github/workflows/sanity.yml +++ b/.github/workflows/sanity.yml @@ -6,7 +6,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, ubuntu-latest, ubuntu-16.04] + os: [macos-latest, ubuntu-latest, ubuntu-18.04] cc: [gcc, clang] include: - os: macos-latest @@ -33,4 +33,4 @@ jobs: uses: actions/upload-artifact@v1 with: name: sameboy-canary-${{ matrix.os }}-${{ matrix.cc }} - path: build/bin \ No newline at end of file + path: build/bin diff --git a/BESS.md b/BESS.md new file mode 100644 index 0000000..7c9296e --- /dev/null +++ b/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. +* Object 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/BootROMs/cgb0_boot.asm b/BootROMs/cgb0_boot.asm new file mode 100644 index 0000000..d49166d --- /dev/null +++ b/BootROMs/cgb0_boot.asm @@ -0,0 +1,2 @@ +CGB0 EQU 1 +include "cgb_boot.asm" \ No newline at end of file diff --git a/BootROMs/cgb_boot.asm b/BootROMs/cgb_boot.asm index 1345915..ca3b57f 100644 --- a/BootROMs/cgb_boot.asm +++ b/BootROMs/cgb_boot.asm @@ -23,13 +23,16 @@ Start: dec c jr nz, .clearOAMLoop +IF !DEF(CGB0) ; Init waveform ld c, $10 + ld hl, $FF30 .waveformLoop ldi [hl], a cpl dec c jr nz, .waveformLoop +ENDC ; Clear chosen input palette ldh [InputPalette], a @@ -44,7 +47,6 @@ Start: ldh [$25], a ld a, $77 ldh [$24], a - ld hl, $FF30 ; Init BG palette ld a, $fc @@ -190,10 +192,9 @@ ENDC IF !DEF(FAST) call DoIntroAnimation - ld a, 45 + ld a, 48 ; frames to wait after playing the chime ldh [WaitLoopCounter], a -; Wait ~0.75 seconds - ld b, a + ld b, 4 ; frames to wait before playing the chime call WaitBFrames ; Play first sound @@ -271,7 +272,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 +331,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 +375,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 +476,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 +919,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 +982,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 @@ -1184,7 +1188,7 @@ ChangeAnimationPalette: call WaitFrame call LoadPalettesFromHRAM ; Delay the wait loop while the user is selecting a palette - ld a, 45 + ld a, 48 ldh [WaitLoopCounter], a pop de pop bc diff --git a/BootROMs/dmg_boot.asm b/BootROMs/dmg_boot.asm index 97a12e7..5517683 100644 --- a/BootROMs/dmg_boot.asm +++ b/BootROMs/dmg_boot.asm @@ -115,7 +115,11 @@ Start: call WaitBFrames ; Set registers to match the original DMG boot +IF DEF(MGB) + ld hl, $FFB0 +ELSE ld hl, $01B0 +ENDC push hl pop af ld hl, $014D diff --git a/BootROMs/mgb_boot.asm b/BootROMs/mgb_boot.asm new file mode 100644 index 0000000..3a98aef --- /dev/null +++ b/BootROMs/mgb_boot.asm @@ -0,0 +1,2 @@ +MGB EQU 1 +include "dmg_boot.asm" \ No newline at end of file diff --git a/BootROMs/pb12.c b/BootROMs/pb12.c index 3a72fab..cfedf6b 100644 --- a/BootROMs/pb12.c +++ b/BootROMs/pb12.c @@ -4,7 +4,6 @@ #include #include #include -#include void opts(uint8_t byte, uint8_t *options) { @@ -18,7 +17,8 @@ void write_all(int fd, const void *buf, size_t count) { while (count) { ssize_t written = write(fd, buf, count); if (written < 0) { - err(1, "write"); + fprintf(stderr, "write"); + exit(1); } count -= written; buf += written; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..94627d1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# SameBoy Coding and Contribution Guidelines + +## Issues + +GitHub Issues are the most effective way to report a bug or request a feature in SameBoy. When reporting a bug, make sure you use the latest stable release, and make sure you mention the SameBoy frontend (Cocoa, SDL, Libretro) and operating system you're using. If you're using Linux/BSD/etc, or you build your own copy of SameBoy for another reason, give as much details as possible on your environment. + +If your bug involves a crash, please attach a crash log or a core dump. If you're using Linux/BSD/etc, or if you're using the Libretro core, please attach the `sameboy` binary (or `libretro_sameboy` library) in that case. + +If your bug is a regression, it'd be extremely helpful if you can report the the first affected version. You get extra credits if you use `git bisect` to point the exact breaking commit. + +If your bug is an emulation bug (Such as a failing test ROM), and you have access to a Game Boy you can test on, please confirm SameBoy is indeed behaving differently from hardware, and report both the emulated model and revision in SameBoy, and the hardware revision you're testing on. + +If your issue is a feature request, demonstrating use cases can help me better prioritize it. + +## Pull Requests + +To allow quicker integration into SameBoy's master branch, contributors are asked to follow SameBoy's style and coding guidelines. Keep in mind that despite the seemingly strict guidelines, all pull requests are welcome – not following the guidelines does not mean your pull request will not be accepted, but it will require manual tweaks from my side for integrating. + +### Languages and Compilers + +SameBoy's core, SDL frontend, Libretro frontend, and automatic tester (Folders `Core`, `SDL` & `OpenDialog`, `libretro`, and `Tester`; respectively) are all written in C11. The Cocoa frontend, SameBoy's fork of Hex Fiend, JoyKit and the Quick Look previewer (Folders `Cocoa`, `HexFiend`, `JoyKit` and `QuickLook`; respectively) are all written in ARC-enabled Objective-C. The SameBoot ROMs (Under `BootROMs`) are written in rgbds-flavor SM83 assembly, with build tools in C11. The shaders (inside `Shaders`) are written in a polyglot GLSL and Metal style, with a few GLSL- and Metal-specific sources. The build system uses standalone Make, in the GNU flavor. Avoid adding new languages (C++, Swift, Python, CMake...) to any of the existing sub-projects. + +SameBoy's main target compiler is Clang, but GCC is also supported when targeting Linux and Libretro. Other compilers (e.g. MSVC) are not supported, and unless there's a good reason, there's no need to go out of your way to add specific support for them. Extensions that are supported by both compilers (Such as `typeof`) may be used if it makes sense. It's OK if you can't test one of these compilers yourself; once you push a commit, the CI bot will let you know if you broke something. + +### Third Party Libraries and Tools + +Avoid adding new required dependencies; run-time and compile-time dependencies alike. Most importantly, avoid linking against GPL licensed libraries (LGPL libraries are fine), so SameBoy can retain its MIT license. + +### Spacing, Indentation and Formatting + +In all files and languages (Other than Makefiles when required), 4 spaces are used for indentation. Unix line endings (`\n`) are used exclusively, even in Windows-specific source files. (`\r` and `\t` shouldn't appear in any source file). Opening braces belong on the same line as their control flow directive, and on their own line when following a function prototype. The `else` keyword always starts on its own line. The `case` keyword is indented relative to its `switch` block, and the code inside a `case` is indented relative to its label. A control flow keyword should have a space between it and the following `(`, commas should follow a space, and operator (except `.` and `->`) should be surrounded by spaces. + +Control flow statements must use `{}`, with the exception of `if` statements that only contain a single `break`, `continue`, or trivial `return` statements. If `{}`s are omitted, the statement must be on the same line as the `if` condition. Functions that do not have any argument must be specified as `(void)`, as mandated by the C standard. The `sizeof` and `typeof` operators should be used as if they're functions (With `()`). `*`, when used to declare pointer types (including functions that return pointers), and when used to dereference a pointer, is attached to the right side (The variable name) – not to the left, and not with spaces on both sides. + +No strict limitations on a line's maximum width, but use your best judgement if you think a statement would benefit from an additional line break. + +Well formatted code example: + +``` +static void my_function(void) +{ + GB_something_t *thing = GB_function(&gb, GB_FLAG_ONE | GB_FLAG_TWO, sizeof(thing)); + if (GB_is_thing(thing)) return; + + switch (*thing) { + case GB_QUACK: + // Something + case GB_DUCK: + // Something else + } +} +``` + +Badly formatted code example: +``` +static void my_function(){ + GB_something_t* thing=GB_function(&gb , GB_FLAG_ONE|GB_FLAG_TWO , sizeof thing); + if( GB_is_thing ( thing ) ) + return; + + switch(* thing) + { + case GB_QUACK: + // Something + case GB_DUCK: + // Something else + } +} +``` + +### Other Coding Conventions + +The primitive types to be used in SameBoy are `unsigned` and `signed` (Without the `int` keyword), the `(u)int*_t` types, `char *` for UTF-8 strings, `double` for non-integer numbers, and `bool` for booleans (Including in Objective-C code, avoid `BOOL`). As long as it's not mandated by a 3rd-party API (e.g. `int` when using file descriptors), avoid using other primitive types. Use `const` whenever possible. + +Most C names should be `lower_case_snake_case`. Constants and macros use `UPPER_CASE_SNAKE_CASE`. Type definitions use a `_t` suffix. Type definitions, as well as non-static (exported) core symbols, should be prefixed with `GB_` (SameBoy's core is intended to be used as a library, so it shouldn't contaminate the global namespace without prefixes). Exported symbols that are only meant to be used by other parts of the core should still get the `GB_` prefix, but their header definition should be inside `#ifdef GB_INTERNAL`. + +For Objective-C naming conventions, use Apple's conventions (Some old Objective-C code mixes these with the C naming convention; new code should use Apple's convention exclusively). The name prefix for SameBoy classes and constants is `GB`. JoyKit's prefix is `JOY`, and Hex Fiend's prefix is `HF`. + +In all languages, prefer long, unambiguous names over short ambiguous ones. diff --git a/Cocoa/AppDelegate.h b/Cocoa/AppDelegate.h index 22e0c36..a9b0048 100644 --- a/Cocoa/AppDelegate.h +++ b/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/Cocoa/AppDelegate.m b/Cocoa/AppDelegate.m index 133fab7..fbdbc79 100644 --- a/Cocoa/AppDelegate.m +++ b/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,11 @@ @"GBCGBModel": @(GB_MODEL_CGB_E), @"GBSGBModel": @(GB_MODEL_SGB2), @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY), + + @"GBVolume": @(1.0), + + @"GBMBC7JoystickOverride": @NO, + @"GBMBC7AllowMouse": @YES, }]; [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{ @@ -54,6 +81,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:true]; + } } - (IBAction)toggleDeveloperMode:(id)sender @@ -72,7 +109,7 @@ NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame]; new.origin.x = old.origin.x; new.origin.y = old.origin.y + (old.size.height - new.size.height); - [_preferencesWindow setFrame:new display:YES animate:_preferencesWindow.visible]; + [_preferencesWindow setFrame:new display:true animate:_preferencesWindow.visible]; [_preferencesWindow.contentView addSubview:tab]; } @@ -81,10 +118,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 +148,306 @@ [[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]; + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:true]; +} + +- (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:true]; + }); + 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:true + 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]; + }); + return; + } + + 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/Cocoa/AppIcon.icns b/Cocoa/AppIcon.icns index 0fe8005..92ad4c6 100644 Binary files a/Cocoa/AppIcon.icns and b/Cocoa/AppIcon.icns differ diff --git a/Cocoa/BigSurToolbar.h b/Cocoa/BigSurToolbar.h index ea8b370..9057d34 100644 --- a/Cocoa/BigSurToolbar.h +++ b/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/Cocoa/Document.h b/Cocoa/Document.h index 660d7bc..2cfaa87 100644 --- a/Cocoa/Document.h +++ b/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 *objectsTableView; +@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/Cocoa/Document.m b/Cocoa/Document.m index 653179d..92b348d 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -10,6 +10,9 @@ #include "GBCheatWindowController.h" #include "GBTerminalTextFieldCell.h" #include "BigSurToolbar.h" +#import "GBPaletteEditorController.h" + +#define GB_MODEL_PAL_BIT_OLD 0x1000 /* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */ /* Todo: Split into category files! This is so messy!!! */ @@ -20,6 +23,7 @@ enum model { MODEL_CGB, MODEL_AGB, MODEL_SGB, + MODEL_MGB, }; @interface Document () @@ -28,6 +32,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 +52,7 @@ enum model { bool oamUpdating; NSMutableData *currentPrinterImageData; - enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy} accessory; + enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy, GBAccessoryLinkCable} accessory; bool rom_warning_issued; @@ -64,8 +69,15 @@ enum model { size_t audioBufferSize; size_t audioBufferPosition; size_t audioBufferNeeded; + double _volume; bool borderModeChanged; + + /* Link cable*/ + Document *master; + Document *slave; + signed linkOffset; + bool linkCableBit; } @property GBAudioClient *audioClient; @@ -81,6 +93,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 +176,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; @@ -177,6 +213,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) debugger_input_queue = [[NSMutableArray alloc] init]; console_output_lock = [[NSRecursiveLock alloc] init]; audioLock = [[NSCondition alloc] init]; + _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; } return self; } @@ -206,8 +243,16 @@ 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_MGB: + return GB_MODEL_MGB; case MODEL_AGB: return GB_MODEL_AGB; @@ -216,23 +261,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (void) updatePalette { - switch ([[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]) { - case 1: - GB_set_palette(&gb, &GB_PALETTE_DMG); - break; - - case 2: - GB_set_palette(&gb, &GB_PALETTE_MGB); - break; - - case 3: - GB_set_palette(&gb, &GB_PALETTE_GBL); - break; - - default: - GB_set_palette(&gb, &GB_PALETTE_GREY); - break; - } + GB_set_palette(&gb, [GBPaletteEditorController userPalette]); } - (void) updateBorderMode @@ -255,6 +284,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 +293,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 +307,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:true]; + }); + } [self.view flip]; if (borderModeChanged) { dispatch_sync(dispatch_get_main_queue(), ^{ @@ -293,19 +332,23 @@ 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 (_audioClient.isPlaying) { if (audioBufferPosition == audioBufferSize) { if (audioBufferSize >= 0x4000) { audioBufferPosition = 0; @@ -321,6 +364,10 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) } audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize); } + if (_volume != 1) { + sample->left *= _volume; + sample->right *= _volume; + } audioBuffer[audioBufferPosition++] = *sample; } if (audioBufferPosition == audioBufferNeeded) { @@ -335,26 +382,31 @@ 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) { + _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { [audioLock lock]; if (audioBufferPosition < nFrames) { audioBufferNeeded = nFrames; - [audioLock wait]; + [audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.125]]; } - if (stopping) { + if (stopping || GB_debugger_is_stopped(&gb)) { memset(buffer, 0, nFrames * sizeof(*buffer)); [audioLock unlock]; return; } - if (audioBufferPosition >= nFrames && audioBufferPosition < nFrames + 4800) { + if (audioBufferPosition < nFrames) { + // Not enough audio + memset(buffer, 0, (nFrames - audioBufferPosition) * sizeof(*buffer)); + memcpy(buffer, audioBuffer, audioBufferPosition * sizeof(*buffer)); + audioBufferPosition = 0; + } + else if (audioBufferPosition < nFrames + 4800) { memcpy(buffer, audioBuffer, nFrames * sizeof(*buffer)); memmove(audioBuffer, audioBuffer + nFrames, (audioBufferPosition - nFrames) * sizeof(*buffer)); audioBufferPosition = audioBufferPosition - nFrames; @@ -366,83 +418,137 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [audioLock unlock]; } andSampleRate:96000]; if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { - [self.audioClient start]; + [_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:true]; [[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)); audioBufferPosition = audioBufferNeeded; [audioLock signal]; [audioLock unlock]; - [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]); + [_audioClient stop]; + _audioClient = nil; + self.view.mouseHidingEnabled = false; + 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)) { @@ -464,12 +570,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (void) loadBootROM: (GB_boot_rom_t)type { static NSString *const names[] = { - [GB_BOOT_ROM_DMG0] = @"dmg0_boot", + [GB_BOOT_ROM_DMG_0] = @"dmg0_boot", [GB_BOOT_ROM_DMG] = @"dmg_boot", [GB_BOOT_ROM_MGB] = @"mgb_boot", [GB_BOOT_ROM_SGB] = @"sgb_boot", [GB_BOOT_ROM_SGB2] = @"sgb2_boot", - [GB_BOOT_ROM_CGB0] = @"cgb0_boot", + [GB_BOOT_ROM_CGB_0] = @"cgb0_boot", [GB_BOOT_ROM_CGB] = @"cgb_boot", [GB_BOOT_ROM_AGB] = @"agb_boot", }; @@ -485,12 +591,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) current_model = (enum model)[sender tag]; } - if (!modelsChanging && [sender tag] == MODEL_NONE) { - GB_reset(&gb); - } - else { - GB_switch_model_and_reset(&gb, [self internalModel]); - } + GB_switch_model_and_reset(&gb, [self internalModel]); if (old_width != GB_get_screen_width(&gb)) { [self.view screenSizeChanged]; @@ -503,6 +604,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_DMG forKey:@"EmulateDMG"]; [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_SGB forKey:@"EmulateSGB"]; [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_AGB forKey:@"EmulateAGB"]; + [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_MGB forKey:@"EmulateMGB"]; } /* Reload the ROM, SAV and SYM files */ @@ -515,10 +617,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]; } @@ -573,12 +683,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) window_frame.size.width); window_frame.size.height = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowHeight"], window_frame.size.height); - [self.mainWindow setFrame:window_frame display:YES]; + [self.mainWindow setFrame:window_frame display:true]; 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:true animate:false]; - - 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 +716,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 +751,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" @@ -642,22 +772,37 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) name:@"GBCGBModelChanged" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateVolume) + name:@"GBVolumeChanged" + object:nil]; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) { current_model = MODEL_DMG; } else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateSGB"]) { current_model = MODEL_SGB; } + else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateMGB"]) { + current_model = MODEL_MGB; + } else { current_model = [[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateAGB"]? MODEL_AGB : MODEL_CGB; } [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 @@ -708,7 +853,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) + (BOOL)autosavesInPlace { - return YES; + return true; } - (NSString *)windowNibName @@ -720,39 +865,134 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type { - return YES; + return true; } -- (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 copy]) { + [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 (!_audioClient.isPlaying) { + [_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,31 +1000,35 @@ 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]; } - (IBAction)mute:(id)sender { - if (self.audioClient.isPlaying) { - [self.audioClient stop]; + if (_audioClient.isPlaying) { + [_audioClient stop]; } else { - [self.audioClient start]; + [_audioClient start]; + if (_volume == 0) { + [GBWarningPopover popoverWithContents:@"Warning: Volume is set to to zero in the preferences panel" onWindow:self.mainWindow]; + } } - [[NSUserDefaults standardUserDefaults] setBool:!self.audioClient.isPlaying forKey:@"Mute"]; + [[NSUserDefaults standardUserDefaults] setBool:!_audioClient.isPlaying forKey:@"Mute"]; } - (BOOL)validateUserInterfaceItem:(id)anItem { if ([anItem action] == @selector(mute:)) { - [(NSMenuItem*)anItem setState:!self.audioClient.isPlaying]; + [(NSMenuItem *)anItem setState:!_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,9 +1048,20 @@ 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)]; } + else if ([anItem action] == @selector(toggleDisplayBackground:)) { + [(NSMenuItem*)anItem setState:!GB_is_background_rendering_disabled(&gb)]; + } + else if ([anItem action] == @selector(toggleDisplayObjects:)) { + [(NSMenuItem*)anItem setState:!GB_is_object_rendering_disabled(&gb)]; + } + return [super validateUserInterfaceItem:anItem]; } @@ -820,7 +1075,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (void) windowWillExitFullScreen:(NSNotification *)notification { fullScreen = false; - self.view.mouseHidingEnabled = NO; + self.view.mouseHidingEnabled = false; } - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame @@ -915,14 +1170,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) } if (![console_output_timer isValid]) { - console_output_timer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:NO]; + console_output_timer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false]; [[NSRunLoop mainRunLoop] addTimer:console_output_timer forMode:NSDefaultRunLoopMode]; } [console_output_lock unlock]; /* Make sure mouse is not hidden while debugging */ - self.view.mouseHidingEnabled = NO; + self.view.mouseHidingEnabled = false; } - (IBAction)showConsoleWindow:(id)sender @@ -1000,6 +1255,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 +1297,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]; } } @@ -1077,7 +1354,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (uint8_t) readMemory:(uint16_t)addr { while (!GB_is_inited(&gb)); - return GB_read_memory(&gb, addr); + return GB_safe_read_memory(&gb, addr); } - (void) writeMemory:(uint16_t)addr value:(uint8_t)value @@ -1090,6 +1367,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]; } @@ -1124,7 +1404,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) bitmapInfo, provider, NULL, - YES, + true, renderingIntent); CGDataProviderRelease(provider); CGColorSpaceRelease(colorSpaceRef); @@ -1145,6 +1425,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (IBAction) reloadVRAMData: (id) sender { if (self.vramWindow.isVisible) { + uint8_t *io_regs = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL); switch ([self.vramTabView.tabViewItems indexOfObject:self.vramTabView.selectedTabViewItem]) { case 0: /* Tileset */ @@ -1183,8 +1464,8 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem, (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem); - self.tilemapImageView.scrollRect = NSMakeRect(GB_read_memory(&gb, 0xFF00 | GB_IO_SCX), - GB_read_memory(&gb, 0xFF00 | GB_IO_SCY), + self.tilemapImageView.scrollRect = NSMakeRect(io_regs[GB_IO_SCX], + io_regs[GB_IO_SCY], 160, 144); self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0]; self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest; @@ -1198,7 +1479,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) dispatch_async(dispatch_get_main_queue(), ^{ if (!oamUpdating) { oamUpdating = true; - [self.spritesTableView reloadData]; + [self.objectsTableView reloadData]; oamUpdating = false; } }); @@ -1332,7 +1613,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) } [self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]]; [hex_controller reloadData]; - [self.memoryView setNeedsDisplay:YES]; + [self.memoryView setNeedsDisplay:true]; } - (GB_gameboy_t *) gameboy @@ -1342,12 +1623,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) + (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName { - return YES; + return true; } - (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, *)) { @@ -1483,14 +1764,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) window_rect.size.height = 512 + height_diff + 48; break; case 3: - window_rect.size.height = 20 * 16 + height_diff + 24; + window_rect.size.height = 20 * 16 + height_diff + 34; break; default: break; } window_rect.origin.y -= window_rect.size.height; - [self.vramWindow setFrame:window_rect display:YES animate:YES]; + [self.vramWindow setFrame:window_rect display:true animate:true]; } - (void)mouseDidLeaveImageView:(GBImageView *)view @@ -1561,7 +1842,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) if (tableView == self.paletteTableView) { return 16; /* 8 BG palettes, 8 OBJ palettes*/ } - else if (tableView == self.spritesTableView) { + else if (tableView == self.objectsTableView) { return oamCount; } return 0; @@ -1580,12 +1861,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) uint16_t index = columnIndex - 1 + (row & 7) * 4; return @((palette_data[(index << 1) + 1] << 8) | palette_data[(index << 1)]); } - else if (tableView == self.spritesTableView) { + else if (tableView == self.objectsTableView) { switch (columnIndex) { case 0: return [Document imageFromData:[NSData dataWithBytesNoCopy:oamInfo[row].image - length:64 * 4 - freeWhenDone:NO] + length:64 * 4 * 2 + freeWhenDone:false] width:8 height:oamHeight scale:16.0/oamHeight]; @@ -1614,7 +1895,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) oamInfo[row].flags & 0x20? 'X' : '-', oamInfo[row].flags & 0x10? 1 : 0]; case 7: - return oamInfo[row].obscured_by_line_limit? @"Dropped: Too many sprites in line": @""; + return oamInfo[row].obscured_by_line_limit? @"Dropped: Too many objects in line": @""; } } @@ -1623,12 +1904,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row { - return tableView == self.spritesTableView; + return tableView == self.objectsTableView; } - (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { - return NO; + return false; } - (IBAction)showVRAMViewer:(id)sender @@ -1658,7 +1939,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) frame.size = self.feedImageView.image.size; [self.printerFeedWindow setContentMaxSize:frame.size]; frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height; - [self.printerFeedWindow setFrame:frame display:NO animate: self.printerFeedWindow.isVisible]; + [self.printerFeedWindow setFrame:frame display:false animate: self.printerFeedWindow.isVisible]; [self.printerFeedWindow orderFront:NULL]; }); @@ -1678,19 +1959,19 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) { bool shouldResume = running; [self stop]; - NSSavePanel * savePanel = [NSSavePanel savePanel]; + 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:@{}]; - [data writeToURL:savePanel.URL atomically:NO]; - [self.printerFeedWindow setIsVisible:NO]; + NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; + [data writeToURL:savePanel.URL atomically:false]; + [self.printerFeedWindow setIsVisible:false]; } if (shouldResume) { [self start]; @@ -1700,6 +1981,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 +1990,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,12 +1999,18 @@ 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); }]; } +- (void) updateVolume +{ + _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; +} + - (void) updateHighpassFilter { if (GB_is_inited(&gb)) { @@ -1736,6 +2025,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 +2053,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; @@ -1787,9 +2097,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview; { if ([[splitView arrangedSubviews] lastObject] == subview) { - return YES; + return true; } - return NO; + return false; } - (CGFloat)splitView:(GBSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex @@ -1805,9 +2115,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) - (BOOL)splitView:(GBSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view { if ([[splitView arrangedSubviews] lastObject] == view) { - return NO; + return false; } - return YES; + return true; } - (void)splitViewDidResizeSubviews:(NSNotification *)notification @@ -1832,4 +2142,204 @@ 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; +} + +- (NSImage *)takeScreenshot +{ + NSImage *ret = nil; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]) { + ret = [_view renderToImage]; + [ret lockFocus]; + NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0, + ret.size.width, ret.size.height)]; + [ret unlockFocus]; + ret = [[NSImage alloc] initWithSize:ret.size]; + [ret addRepresentation:bitmapRep]; + } + if (!ret) { + ret = [Document imageFromData:[NSData dataWithBytesNoCopy:_view.currentBuffer + length:GB_get_screen_width(&gb) * GB_get_screen_height(&gb) * 4 + freeWhenDone:false] + width:GB_get_screen_width(&gb) + height:GB_get_screen_height(&gb) + scale:1.0]; + } + return ret; +} + +- (NSString *)screenshotFilename +{ + NSDate *date = [NSDate date]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterLongStyle; + dateFormatter.timeStyle = NSDateFormatterMediumStyle; + return [[NSString stringWithFormat:@"%@ – %@.png", + self.fileURL.lastPathComponent.stringByDeletingPathExtension, + [dateFormatter stringFromDate:date]] stringByReplacingOccurrencesOfString:@":" withString:@"."]; // Gotta love Mac OS Classic + +} + +- (IBAction)saveScreenshot:(id)sender +{ + NSString *folder = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBScreenshotFolder"]; + BOOL isDirectory = false; + if (folder) { + [[NSFileManager defaultManager] fileExistsAtPath:folder isDirectory:&isDirectory]; + } + if (!folder) { + bool shouldResume = running; + [self stop]; + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + openPanel.canChooseFiles = false; + openPanel.canChooseDirectories = true; + openPanel.message = @"Choose a folder for screenshots"; + [openPanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { + if (result == NSModalResponseOK) { + [[NSUserDefaults standardUserDefaults] setObject:openPanel.URL.path + forKey:@"GBScreenshotFolder"]; + [self saveScreenshot:sender]; + } + if (shouldResume) { + [self start]; + } + + }]; + return; + } + NSImage *image = [self takeScreenshot]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterLongStyle; + dateFormatter.timeStyle = NSDateFormatterMediumStyle; + NSString *filename = [self screenshotFilename]; + filename = [folder stringByAppendingPathComponent:filename]; + unsigned i = 2; + while ([[NSFileManager defaultManager] fileExistsAtPath:filename]) { + filename = [[filename stringByDeletingPathExtension] stringByAppendingFormat:@" %d.png", i++]; + } + + NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject; + NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; + [data writeToFile:filename atomically:false]; + [self.osdView displayText:@"Screenshot saved"]; +} + +- (IBAction)saveScreenshotAs:(id)sender +{ + bool shouldResume = running; + [self stop]; + NSImage *image = [self takeScreenshot]; + NSSavePanel *savePanel = [NSSavePanel savePanel]; + [savePanel setNameFieldStringValue:[self screenshotFilename]]; + [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { + if (result == NSModalResponseOK) { + [savePanel orderOut:self]; + NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject; + NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; + [data writeToURL:savePanel.URL atomically:false]; + [[NSUserDefaults standardUserDefaults] setObject:savePanel.URL.path.stringByDeletingLastPathComponent + forKey:@"GBScreenshotFolder"]; + } + if (shouldResume) { + [self start]; + } + }]; + [self.osdView displayText:@"Screenshot saved"]; +} + +- (IBAction)copyScreenshot:(id)sender +{ + NSImage *image = [self takeScreenshot]; + [[NSPasteboard generalPasteboard] clearContents]; + [[NSPasteboard generalPasteboard] writeObjects:@[image]]; + [self.osdView displayText:@"Screenshot copied"]; +} + +- (IBAction)toggleDisplayBackground:(id)sender +{ + GB_set_background_rendering_disabled(&gb, !GB_is_background_rendering_disabled(&gb)); +} + +- (IBAction)toggleDisplayObjects:(id)sender +{ + GB_set_object_rendering_disabled(&gb, !GB_is_object_rendering_disabled(&gb)); +} + @end diff --git a/Cocoa/Document.xib b/Cocoa/Document.xib index d02f5bd..4c98007 100644 --- a/Cocoa/Document.xib +++ b/Cocoa/Document.xib @@ -25,9 +25,10 @@ + + - @@ -59,9 +60,16 @@ + + + + + + + @@ -115,7 +123,7 @@ - + @@ -152,7 +160,7 @@ - + @@ -186,7 +194,7 @@ - + @@ -494,7 +502,7 @@ - + @@ -762,7 +770,7 @@ - + @@ -996,7 +1004,7 @@ - + @@ -1021,7 +1029,7 @@ - + diff --git a/Cocoa/GBAudioClient.h b/Cocoa/GBAudioClient.h index aa7be8c..03ed701 100644 --- a/Cocoa/GBAudioClient.h +++ b/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/Cocoa/GBAudioClient.m b/Cocoa/GBAudioClient.m index 7f2115d..81a51fd 100644 --- a/Cocoa/GBAudioClient.m +++ b/Cocoa/GBAudioClient.m @@ -90,7 +90,7 @@ static OSStatus render( { OSErr err = AudioOutputUnitStart(audioUnit); NSAssert1(err == noErr, @"Error starting unit: %hd", err); - _playing = YES; + _playing = true; } @@ -98,7 +98,7 @@ static OSStatus render( -(void) stop { AudioOutputUnitStop(audioUnit); - _playing = NO; + _playing = false; } -(void) dealloc @@ -108,4 +108,4 @@ static OSStatus render( AudioComponentInstanceDispose(audioUnit); } -@end \ No newline at end of file +@end diff --git a/Cocoa/GBBorderView.m b/Cocoa/GBBorderView.m index a5f5e81..d992e29 100644 --- a/Cocoa/GBBorderView.m +++ b/Cocoa/GBBorderView.m @@ -5,12 +5,12 @@ - (void)awakeFromNib { - self.wantsLayer = YES; + self.wantsLayer = true; } - (BOOL)wantsUpdateLayer { - return YES; + return true; } - (void)updateLayer diff --git a/Cocoa/GBCheatTextFieldCell.h b/Cocoa/GBCheatTextFieldCell.h index 473e0f3..e7fd917 100644 --- a/Cocoa/GBCheatTextFieldCell.h +++ b/Cocoa/GBCheatTextFieldCell.h @@ -1,5 +1,5 @@ #import @interface GBCheatTextFieldCell : NSTextFieldCell -@property bool usesAddressFormat; +@property (nonatomic) bool usesAddressFormat; @end diff --git a/Cocoa/GBCheatTextFieldCell.m b/Cocoa/GBCheatTextFieldCell.m index 611cade..1fdafea 100644 --- a/Cocoa/GBCheatTextFieldCell.m +++ b/Cocoa/GBCheatTextFieldCell.m @@ -114,7 +114,7 @@ return _fieldEditor; } _fieldEditor = [[GBCheatTextView alloc] initWithFrame:controlView.frame]; - _fieldEditor.fieldEditor = YES; + _fieldEditor.fieldEditor = true; _fieldEditor.usesAddressFormat = self.usesAddressFormat; return _fieldEditor; } diff --git a/Cocoa/GBCheatWindowController.h b/Cocoa/GBCheatWindowController.h index f70553e..eddebc5 100644 --- a/Cocoa/GBCheatWindowController.h +++ b/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/Cocoa/GBCheatWindowController.m b/Cocoa/GBCheatWindowController.m index c10e2a9..5cc8f59 100644 --- a/Cocoa/GBCheatWindowController.m +++ b/Cocoa/GBCheatWindowController.m @@ -52,7 +52,7 @@ if (row >= cheatCount) { switch (columnIndex) { case 0: - return @(YES); + return @YES; case 1: return @NO; @@ -67,7 +67,7 @@ switch (columnIndex) { case 0: - return @(NO); + return @NO; case 1: return @(cheats[row]->enabled); diff --git a/Cocoa/GBColorCell.m b/Cocoa/GBColorCell.m index 0ad102a..761ad43 100644 --- a/Cocoa/GBColorCell.m +++ b/Cocoa/GBColorCell.m @@ -13,13 +13,13 @@ static inline double scale_channel(uint8_t x) - (void)setObjectValue:(id)objectValue { - _integerValue = [objectValue integerValue]; uint8_t r = _integerValue & 0x1F, g = (_integerValue >> 5) & 0x1F, b = (_integerValue >> 10) & 0x1F; super.objectValue = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"$%04x", (uint16_t)(_integerValue & 0x7FFF)] attributes:@{ - NSForegroundColorAttributeName: r * 3 + g * 4 + b * 2 > 120? [NSColor blackColor] : [NSColor whiteColor] + NSForegroundColorAttributeName: r * 3 + g * 4 + b * 2 > 120? [NSColor blackColor] : [NSColor whiteColor], + NSFontAttributeName: [NSFont userFixedPitchFontOfSize:12] }]; } @@ -36,13 +36,14 @@ static inline double scale_channel(uint8_t x) - (NSColor *) backgroundColor { + /* Todo: color correction */ uint16_t color = self.integerValue; return [NSColor colorWithRed:scale_channel(color) green:scale_channel(color >> 5) blue:scale_channel(color >> 10) alpha:1.0]; } - (BOOL)drawsBackground { - return YES; + return true; } @end diff --git a/Cocoa/GBHueSliderCell.h b/Cocoa/GBHueSliderCell.h new file mode 100644 index 0000000..f293124 --- /dev/null +++ b/Cocoa/GBHueSliderCell.h @@ -0,0 +1,9 @@ +#import + +@interface NSSlider (GBHueSlider) +-(NSColor *)colorValue; +@end + +@interface GBHueSliderCell : NSSliderCell +-(NSColor *)colorValue; +@end diff --git a/Cocoa/GBHueSliderCell.m b/Cocoa/GBHueSliderCell.m new file mode 100644 index 0000000..9b397cb --- /dev/null +++ b/Cocoa/GBHueSliderCell.m @@ -0,0 +1,113 @@ +#import "GBHueSliderCell.h" + +@interface NSSliderCell(privateAPI) +- (double)_normalizedDoubleValue; +@end + +@implementation GBHueSliderCell +{ + bool _drawingTrack; +} + +-(NSColor *)colorValue +{ + double hue = self.doubleValue / 360.0; + double r = 0, g = 0, b =0 ; + double t = fmod(hue * 6, 1); + switch ((int)(hue * 6) % 6) { + case 0: + r = 1; + g = t; + break; + case 1: + r = 1 - t; + g = 1; + break; + case 2: + g = 1; + b = t; + break; + case 3: + g = 1 - t; + b = 1; + break; + case 4: + b = 1; + r = t; + break; + case 5: + b = 1 - t; + r = 1; + break; + } + return [NSColor colorWithRed:r green:g blue:b alpha:1.0]; +} + +-(void)drawKnob:(NSRect)knobRect +{ + [super drawKnob:knobRect]; + NSRect peekRect = knobRect; + peekRect.size.width /= 2; + peekRect.size.height = peekRect.size.width; + peekRect.origin.x += peekRect.size.width / 2; + peekRect.origin.y += peekRect.size.height / 2; + NSColor *color = self.colorValue; + if (!self.enabled) { + color = [color colorWithAlphaComponent:0.5]; + } + [color setFill]; + NSBezierPath *path = [NSBezierPath bezierPathWithOvalInRect:peekRect]; + [path fill]; + [[NSColor colorWithWhite:0 alpha:0.25] setStroke]; + [path setLineWidth:0.5]; + [path stroke]; +} + +-(double)_normalizedDoubleValue +{ + if (_drawingTrack) return 0; + return [super _normalizedDoubleValue]; +} + +-(void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped +{ + if (!self.enabled) { + [super drawBarInside:rect flipped:flipped]; + return; + } + + _drawingTrack = true; + [super drawBarInside:rect flipped:flipped]; + _drawingTrack = false; + + NSGradient *gradient = [[NSGradient alloc] initWithColors:@[ + [NSColor redColor], + [NSColor yellowColor], + [NSColor greenColor], + [NSColor cyanColor], + [NSColor blueColor], + [NSColor magentaColor], + [NSColor redColor], + ]]; + + rect.origin.y += rect.size.height / 2 - 0.5; + rect.size.height = 1; + rect.size.width -= 2; + rect.origin.x += 1; + [[NSColor redColor] set]; + NSRectFill(rect); + + rect.size.width -= self.knobThickness + 2; + rect.origin.x += self.knobThickness / 2 - 1; + + [gradient drawInRect:rect angle:0]; +} + +@end + +@implementation NSSlider (GBHueSlider) +- (NSColor *)colorValue +{ + return ((GBHueSliderCell *)self.cell).colorValue; +} +@end diff --git a/Cocoa/GBImageView.h b/Cocoa/GBImageView.h index d5ee534..c15c4e7 100644 --- a/Cocoa/GBImageView.h +++ b/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/Cocoa/GBMemoryByteArray.h b/Cocoa/GBMemoryByteArray.h index e3ed71f..17abe20 100644 --- a/Cocoa/GBMemoryByteArray.h +++ b/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/Cocoa/GBOSDView.h b/Cocoa/GBOSDView.h new file mode 100644 index 0000000..4771d2f --- /dev/null +++ b/Cocoa/GBOSDView.h @@ -0,0 +1,6 @@ +#import + +@interface GBOSDView : NSView +@property bool usesSGBScale; +- (void)displayText:(NSString *)text; +@end diff --git a/Cocoa/GBOSDView.m b/Cocoa/GBOSDView.m new file mode 100644 index 0000000..710229e --- /dev/null +++ b/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/Cocoa/GBOpenGLView.h b/Cocoa/GBOpenGLView.h index 5f875ab..66001c9 100644 --- a/Cocoa/GBOpenGLView.h +++ b/Cocoa/GBOpenGLView.h @@ -2,5 +2,5 @@ #import "GBGLShader.h" @interface GBOpenGLView : NSOpenGLView -@property GBGLShader *shader; +@property (nonatomic) GBGLShader *shader; @end diff --git a/Cocoa/GBOpenGLView.m b/Cocoa/GBOpenGLView.m index 90ebf8d..2e4eb70 100644 --- a/Cocoa/GBOpenGLView.m +++ b/Cocoa/GBOpenGLView.m @@ -34,6 +34,6 @@ - (void) filterChanged { self.shader = nil; - [self setNeedsDisplay:YES]; + [self setNeedsDisplay:true]; } @end diff --git a/Cocoa/GBPaletteEditorController.h b/Cocoa/GBPaletteEditorController.h new file mode 100644 index 0000000..fd362ec --- /dev/null +++ b/Cocoa/GBPaletteEditorController.h @@ -0,0 +1,18 @@ +#import +#import + +@interface GBPaletteEditorController : NSObject +@property (weak) IBOutlet NSColorWell *colorWell0; +@property (weak) IBOutlet NSColorWell *colorWell1; +@property (weak) IBOutlet NSColorWell *colorWell2; +@property (weak) IBOutlet NSColorWell *colorWell3; +@property (weak) IBOutlet NSColorWell *colorWell4; +@property (weak) IBOutlet NSButton *disableLCDColorCheckbox; +@property (weak) IBOutlet NSButton *manualModeCheckbox; +@property (weak) IBOutlet NSSlider *brightnessSlider; +@property (weak) IBOutlet NSSlider *hueSlider; +@property (weak) IBOutlet NSSlider *hueStrengthSlider; +@property (weak) IBOutlet NSTableView *themesList; +@property (weak) IBOutlet NSMenu *menu; ++ (const GB_palette_t *)userPalette; +@end diff --git a/Cocoa/GBPaletteEditorController.m b/Cocoa/GBPaletteEditorController.m new file mode 100644 index 0000000..0a613fb --- /dev/null +++ b/Cocoa/GBPaletteEditorController.m @@ -0,0 +1,378 @@ +#import "GBPaletteEditorController.h" +#import "GBHueSliderCell.h" +#import + +#define MAGIC 'SBPL' + +typedef struct __attribute__ ((packed)) { + uint32_t magic; + bool manual:1; + bool disabled_lcd_color:1; + unsigned padding:6; + struct GB_color_s colors[5]; + int32_t brightness_bias; + uint32_t hue_bias; + uint32_t hue_bias_strength; +} theme_t; + +static double blend(double from, double to, double position) +{ + return from * (1 - position) + to * position; +} + +@implementation NSColor (GBColor) + +- (struct GB_color_s)gbColor +{ + NSColor *sRGB = [self colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + return (struct GB_color_s){round(sRGB.redComponent * 255), round(sRGB.greenComponent * 255), round(sRGB.blueComponent * 255)}; +} + +- (uint32_t)intValue +{ + struct GB_color_s color = self.gbColor; + return (color.r << 0) | (color.g << 8) | (color.b << 16) | 0xFF000000; +} + +@end + +@implementation GBPaletteEditorController + +- (NSArray *)colorWells +{ + return @[_colorWell0, _colorWell1, _colorWell2, _colorWell3, _colorWell4]; +} + +- (void)updateEnabledControls +{ + if (self.manualModeCheckbox.state) { + _brightnessSlider.enabled = false; + _hueSlider.enabled = false; + _hueStrengthSlider.enabled = false; + _colorWell1.enabled = true; + _colorWell2.enabled = true; + _colorWell3.enabled = true; + if (!(_colorWell4.enabled = self.disableLCDColorCheckbox.state)) { + _colorWell4.color = _colorWell3.color; + } + } + else { + _colorWell1.enabled = false; + _colorWell2.enabled = false; + _colorWell3.enabled = false; + _colorWell4.enabled = true; + _brightnessSlider.enabled = true; + _hueSlider.enabled = true; + _hueStrengthSlider.enabled = true; + [self updateAutoColors]; + } +} + +- (NSColor *)autoColorAtPositon:(double)position +{ + NSColor *first = [_colorWell0.color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + NSColor *second = [_colorWell4.color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + double brightness = 1 / pow(4, (_brightnessSlider.doubleValue - 128) / 128.0); + position = pow(position, brightness); + NSColor *hue = _hueSlider.colorValue; + double bias = _hueStrengthSlider.doubleValue / 256.0; + double red = 1 / pow(4, (hue.redComponent * 2 - 1) * bias); + double green = 1 / pow(4, (hue.greenComponent * 2 - 1) * bias); + double blue = 1 / pow(4, (hue.blueComponent * 2 - 1) * bias); + NSColor *ret = [NSColor colorWithRed:blend(first.redComponent, second.redComponent, pow(position, red)) + green:blend(first.greenComponent, second.greenComponent, pow(position, green)) + blue:blend(first.blueComponent, second.blueComponent, pow(position, blue)) + alpha:1.0]; + return ret; +} + +- (IBAction)updateAutoColors:(id)sender +{ + if (!self.manualModeCheckbox.state) { + [self updateAutoColors]; + } + else { + [self savePalette:sender]; + } +} + +- (void)updateAutoColors +{ + if (_disableLCDColorCheckbox.state) { + _colorWell1.color = [self autoColorAtPositon:8 / 25.0]; + _colorWell2.color = [self autoColorAtPositon:16 / 25.0]; + _colorWell3.color = [self autoColorAtPositon:24 / 25.0]; + } + else { + _colorWell1.color = [self autoColorAtPositon:1 / 3.0]; + _colorWell2.color = [self autoColorAtPositon:2 / 3.0]; + _colorWell3.color = _colorWell4.color; + } + [self savePalette:nil]; +} + +- (IBAction)disabledLCDColorCheckboxChanged:(id)sender +{ + [self updateEnabledControls]; +} + +- (IBAction)manualModeChanged:(id)sender +{ + [self updateEnabledControls]; +} + +- (IBAction)updateColor4:(id)sender +{ + if (!self.disableLCDColorCheckbox.state) { + self.colorWell4.color = self.colorWell3.color; + } + [self savePalette:self]; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + if (themes.count == 0) { + [defaults setObject:@"Untitled Palette" forKey:@"GBCurrentTheme"]; + [self savePalette:nil]; + return 1; + } + return themes.count; +} + +-(void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + NSString *oldName = [self tableView:tableView objectValueForTableColumn:tableColumn row:row]; + if ([oldName isEqualToString:object]) { + return; + } + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *themes = [[defaults dictionaryForKey:@"GBThemes"] ?: @{} mutableCopy]; + NSString *newName = object; + unsigned i = 2; + if (!newName.length) { + newName = @"Untitled Palette"; + } + while (themes[newName]) { + newName = [NSString stringWithFormat:@"%@ %d", object, i]; + } + themes[newName] = themes[oldName]; + [themes removeObjectForKey:oldName]; + if ([oldName isEqualToString:[defaults stringForKey:@"GBCurrentTheme"]]) { + [defaults setObject:newName forKey:@"GBCurrentTheme"]; + } + [defaults setObject:themes forKey:@"GBThemes"]; + [tableView reloadData]; + [self awakeFromNib]; +} + +- (IBAction)deleteTheme:(id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *name = [defaults stringForKey:@"GBCurrentTheme"]; + NSMutableDictionary *themes = [[defaults dictionaryForKey:@"GBThemes"] ?: @{} mutableCopy]; + [themes removeObjectForKey:name]; + [defaults setObject:themes forKey:@"GBThemes"]; + [_themesList reloadData]; + [self awakeFromNib]; +} + +- (void)tableViewSelectionDidChange:(NSNotification *)notification +{ + NSString *name = [self tableView:nil objectValueForTableColumn:nil row:_themesList.selectedRow]; + [[NSUserDefaults standardUserDefaults] setObject:name forKey:@"GBCurrentTheme"]; + [self loadPalette]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorPaletteChanged" object:nil]; +} + +- (void)tableViewSelectionIsChanging:(NSNotification *)notification +{ + [self tableViewSelectionDidChange:notification]; +} + +- (void)awakeFromNib +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSString *theme = [defaults stringForKey:@"GBCurrentTheme"]; + if (theme && themes[theme]) { + unsigned index = [[themes.allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] indexOfObject:theme]; + [_themesList selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:false]; + } + else { + [_themesList selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:false]; + } + [self tableViewSelectionDidChange:nil]; +} + +- (IBAction)addTheme:(id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSString *newName = @"Untitled Palette"; + unsigned i = 2; + while (themes[newName]) { + newName = [NSString stringWithFormat:@"Untitled Palette %d", i++]; + } + [defaults setObject:newName forKey:@"GBCurrentTheme"]; + [self savePalette:sender]; + [_themesList reloadData]; + [self awakeFromNib]; +} + +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + return [themes.allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)][row]; +} + +- (void)loadPalette +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *theme = [defaults dictionaryForKey:@"GBThemes"][[defaults stringForKey:@"GBCurrentTheme"]]; + NSArray *colors = theme[@"Colors"]; + if (colors.count == 5) { + unsigned i = 0; + for (NSNumber *color in colors) { + uint32_t c = [color unsignedIntValue]; + self.colorWells[i++].color = [NSColor colorWithRed:(c & 0xFF) / 255.0 + green:((c >> 8) & 0xFF) / 255.0 + blue:((c >> 16) & 0xFF) / 255.0 + alpha:1.0]; + } + } + _disableLCDColorCheckbox.state = [theme[@"DisabledLCDColor"] boolValue]; + _manualModeCheckbox.state = [theme[@"Manual"] boolValue]; + _brightnessSlider.doubleValue = [theme[@"BrightnessBias"] doubleValue] * 128 + 128; + _hueSlider.doubleValue = [theme[@"HueBias"] doubleValue] * 360; + _hueStrengthSlider.doubleValue = [theme[@"HueBiasStrength"] doubleValue] * 256; + [self updateEnabledControls]; +} + +- (IBAction)savePalette:(id)sender +{ + NSDictionary *theme = @{ + @"Colors": + @[@(_colorWell0.color.intValue), + @(_colorWell1.color.intValue), + @(_colorWell2.color.intValue), + @(_colorWell3.color.intValue), + @(_colorWell4.color.intValue)], + @"DisabledLCDColor": _disableLCDColorCheckbox.state? @YES : @NO, + @"Manual": _manualModeCheckbox.state? @YES : @NO, + @"BrightnessBias": @((_brightnessSlider.doubleValue - 128) / 128.0), + @"HueBias": @(_hueSlider.doubleValue / 360.0), + @"HueBiasStrength": @(_hueStrengthSlider.doubleValue / 256.0) + }; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *themes = [[defaults dictionaryForKey:@"GBThemes"] ?: @{} mutableCopy]; + themes[[defaults stringForKey:@"GBCurrentTheme"]] = theme; + [defaults setObject:themes forKey:@"GBThemes"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorPaletteChanged" object:nil]; +} + ++ (const GB_palette_t *)userPalette +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + switch ([defaults integerForKey:@"GBColorPalette"]) { + case 1: return &GB_PALETTE_DMG; + case 2: return &GB_PALETTE_MGB; + case 3: return &GB_PALETTE_GBL; + default: return &GB_PALETTE_GREY; + case -1: { + static GB_palette_t customPalette; + NSArray *colors = [defaults dictionaryForKey:@"GBThemes"][[defaults stringForKey:@"GBCurrentTheme"]][@"Colors"]; + if (colors.count == 5) { + unsigned i = 0; + for (NSNumber *color in colors) { + uint32_t c = [color unsignedIntValue]; + customPalette.colors[i++] = (struct GB_color_s) {c, c >> 8, c >> 16}; + } + } + return &customPalette; + } + } +} + +- (IBAction)export:(id)sender +{ + NSSavePanel *savePanel = [NSSavePanel savePanel]; + [savePanel setAllowedFileTypes:@[@"sbp"]]; + savePanel.nameFieldStringValue = [NSString stringWithFormat:@"%@.sbp", [[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]]; + if ([savePanel runModal] == NSModalResponseOK) { + theme_t theme = {0,}; + theme.magic = MAGIC; + theme.manual = _manualModeCheckbox.state; + theme.disabled_lcd_color = _disableLCDColorCheckbox.state; + unsigned i = 0; + for (NSColorWell *well in self.colorWells) { + theme.colors[i++] = well.color.gbColor; + } + theme.brightness_bias = (_brightnessSlider.doubleValue - 128) * (0x40000000 / 128); + theme.hue_bias = round(_hueSlider.doubleValue * (0x80000000 / 360.0)); + theme.hue_bias_strength = (_hueStrengthSlider.doubleValue) * (0x80000000 / 256); + size_t size = sizeof(theme); + if (theme.manual) { + size = theme.disabled_lcd_color? 5 + 5 * sizeof(theme.colors[0]) : 5 + 4 * sizeof(theme.colors[0]); + } + [[NSData dataWithBytes:&theme length:size] writeToURL:savePanel.URL atomically:false]; + } +} + +- (IBAction)import:(id)sender +{ + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + [openPanel setAllowedFileTypes:@[@"sbp"]]; + if ([openPanel runModal] == NSModalResponseOK) { + NSData *data = [NSData dataWithContentsOfURL:openPanel.URL]; + theme_t theme = {0,}; + memcpy(&theme, data.bytes, MIN(sizeof(theme), data.length)); + if (theme.magic != MAGIC) { + NSBeep(); + return; + } + _manualModeCheckbox.state = theme.manual; + _disableLCDColorCheckbox.state = theme.disabled_lcd_color; + unsigned i = 0; + for (NSColorWell *well in self.colorWells) { + well.color = [NSColor colorWithRed:theme.colors[i].r / 255.0 + green:theme.colors[i].g / 255.0 + blue:theme.colors[i].b / 255.0 + alpha:1.0]; + i++; + } + if (!theme.disabled_lcd_color) { + _colorWell4.color = _colorWell3.color; + } + _brightnessSlider.doubleValue = theme.brightness_bias / (0x40000000 / 128.0) + 128; + _hueSlider.doubleValue = theme.hue_bias / (0x80000000 / 360.0); + _hueStrengthSlider.doubleValue = theme.hue_bias_strength / (0x80000000 / 256.0); + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSString *baseName = openPanel.URL.lastPathComponent.stringByDeletingPathExtension; + NSString *newName = baseName; + i = 2; + while (themes[newName]) { + newName = [NSString stringWithFormat:@"%@ %d", baseName, i++]; + } + [defaults setObject:newName forKey:@"GBCurrentTheme"]; + [self savePalette:sender]; + [self awakeFromNib]; + } +} + +- (IBAction)done:(NSButton *)sender +{ + [sender.window.sheetParent endSheet:sender.window]; +} + +- (instancetype)init +{ + static id singleton = nil; + if (singleton) return singleton; + return (singleton = [super init]); +} +@end diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h index ee697a8..30b045c 100644 --- a/Cocoa/GBPreferencesWindow.h +++ b/Cocoa/GBPreferencesWindow.h @@ -1,27 +1,37 @@ #import #import +#import "GBPaletteEditorController.h" @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; +@property (weak) IBOutlet NSButton *screenshotFilterCheckbox; +@property (weak) IBOutlet GBPaletteEditorController *paletteEditorController; +@property (strong) IBOutlet NSWindow *paletteEditor; +@property (weak) IBOutlet NSButton *joystickMBC7Checkbox; +@property (weak) IBOutlet NSButton *mouseMBC7Checkbox; @end diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index 491f0c0..02d428a 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -2,6 +2,7 @@ #import "NSString+StringForKey.h" #import "GBButtons.h" #import "BigSurToolbar.h" +#import "GBViewMetal.h" #import @implementation GBPreferencesWindow @@ -19,6 +20,7 @@ NSPopUpButton *_colorPalettePopupButton; NSPopUpButton *_displayBorderPopupButton; NSPopUpButton *_rewindPopupButton; + NSPopUpButton *_rtcPopupButton; NSButton *_aspectRatioCheckbox; NSButton *_analogControlsCheckbox; NSEventModifierFlags previousModifiers; @@ -26,6 +28,14 @@ NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton; NSPopUpButton *_preferredJoypadButton; NSPopUpButton *_rumbleModePopupButton; + NSSlider *_temperatureSlider; + NSSlider *_interferenceSlider; + NSSlider *_volumeSlider; + NSButton *_autoUpdatesCheckbox; + NSButton *_OSDCheckbox; + NSButton *_screenshotFilterCheckbox; + NSButton *_joystickMBC7Checkbox; + NSButton *_mouseMBC7Checkbox; } + (NSArray *)filterList @@ -61,8 +71,8 @@ - (void)close { joystick_configuration_state = -1; - [self.configureJoypadButton setEnabled:YES]; - [self.skipButton setEnabled:NO]; + [self.configureJoypadButton setEnabled:true]; + [self.skipButton setEnabled:false]; [self.configureJoypadButton setTitle:@"Configure Controller"]; [super close]; } @@ -91,11 +101,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; @@ -111,8 +155,14 @@ - (void)setColorPalettePopupButton:(NSPopUpButton *)colorPalettePopupButton { _colorPalettePopupButton = colorPalettePopupButton; + [self updatePalettesMenu]; NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]; - [_colorPalettePopupButton selectItemAtIndex:mode]; + if (mode >= 0) { + [_colorPalettePopupButton selectItemWithTag:mode]; + } + else { + [_colorPalettePopupButton selectItemWithTitle:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] ?: @""]; + } } - (NSPopUpButton *)colorPalettePopupButton @@ -156,6 +206,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; @@ -214,12 +276,12 @@ dispatch_async(dispatch_get_main_queue(), ^{ is_button_being_modified = true; button_being_modified = row; - tableView.enabled = NO; - self.playerListButton.enabled = NO; + tableView.enabled = false; + self.playerListButton.enabled = false; [tableView reloadData]; [self makeFirstResponder:self]; }); - return NO; + return false; } -(void)keyDown:(NSEvent *)theEvent @@ -235,8 +297,8 @@ [[NSUserDefaults standardUserDefaults] setInteger:theEvent.keyCode forKey:button_to_preference_name(button_being_modified, self.playerListButton.selectedTag)]; - self.controlsTableView.enabled = YES; - self.playerListButton.enabled = YES; + self.controlsTableView.enabled = true; + self.playerListButton.enabled = true; [self.controlsTableView reloadData]; [self makeFirstResponder:self.controlsTableView]; } @@ -264,6 +326,19 @@ [[NSNotificationCenter defaultCenter] postNotificationName:@"GBHighpassFilterChanged" object:nil]; } + +- (IBAction)changeMBC7JoystickOverride:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState + forKey:@"GBMBC7JoystickOverride"]; +} + +- (IBAction)changeMBC7AllowMouse:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState + forKey:@"GBMBC7AllowMouse"]; +} + - (IBAction)changeAnalogControls:(id)sender { [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState @@ -284,6 +359,27 @@ [[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"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBVolumeChanged" object:nil]; +} + - (IBAction)franeBlendingModeChanged:(id)sender { [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) @@ -292,10 +388,51 @@ } +- (void)updatePalettesMenu +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSMenu *menu = _colorPalettePopupButton.menu; + while (menu.itemArray.count != 4) { + [menu removeItemAtIndex:4]; + } + [menu addItem:[NSMenuItem separatorItem]]; + for (NSString *name in [themes.allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]) { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:name action:nil keyEquivalent:@""]; + item.tag = -2; + [menu addItem:item]; + } + if (themes) { + [menu addItem:[NSMenuItem separatorItem]]; + } + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Custom…" action:nil keyEquivalent:@""]; + item.tag = -1; + [menu addItem:item]; +} + - (IBAction)colorPaletteChanged:(id)sender { - [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) - forKey:@"GBColorPalette"]; + signed tag = [sender selectedItem].tag; + if (tag == -2) { + [[NSUserDefaults standardUserDefaults] setObject:@(-1) + forKey:@"GBColorPalette"]; + [[NSUserDefaults standardUserDefaults] setObject:[sender selectedItem].title + forKey:@"GBCurrentTheme"]; + + } + else if (tag == -1) { + [[NSUserDefaults standardUserDefaults] setObject:@(-1) + forKey:@"GBColorPalette"]; + [_paletteEditorController awakeFromNib]; + [self beginSheet:_paletteEditor completionHandler:^(NSModalResponse returnCode) { + [self updatePalettesMenu]; + [_colorPalettePopupButton selectItemWithTitle:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] ?: @""]; + }]; + } + else { + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedItem].tag) + forKey:@"GBColorPalette"]; + } [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorPaletteChanged" object:nil]; } @@ -320,10 +457,24 @@ [[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]; - [self.skipButton setEnabled:YES]; + [self.configureJoypadButton setEnabled:false]; + [self.skipButton setEnabled:true]; joystick_being_configured = nil; [self advanceConfigurationStateMachine]; } @@ -344,8 +495,8 @@ } else { joystick_configuration_state = -1; - [self.configureJoypadButton setEnabled:YES]; - [self.skipButton setEnabled:NO]; + [self.configureJoypadButton setEnabled:true]; + [self.skipButton setEnabled:false]; [self.configureJoypadButton setTitle:@"Configure Joypad"]; } } @@ -392,30 +543,36 @@ 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) { 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); } } @@ -430,6 +587,28 @@ [self advanceConfigurationStateMachine]; } +- (NSButton *)joystickMBC7Checkbox +{ + return _joystickMBC7Checkbox; +} + +- (void)setJoystickMBC7Checkbox:(NSButton *)joystickMBC7Checkbox +{ + _joystickMBC7Checkbox = joystickMBC7Checkbox; + [_joystickMBC7Checkbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7JoystickOverride"]]; +} + +- (NSButton *)mouseMBC7Checkbox +{ + return _mouseMBC7Checkbox; +} + +- (void)setMouseMBC7Checkbox:(NSButton *)mouseMBC7Checkbox +{ + _mouseMBC7Checkbox = mouseMBC7Checkbox; + [_mouseMBC7Checkbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7AllowMouse"]]; +} + - (NSButton *)analogControlsCheckbox { return _analogControlsCheckbox; @@ -470,8 +649,8 @@ - (IBAction)selectOtherBootROMFolder:(id)sender { NSOpenPanel *panel = [[NSOpenPanel alloc] init]; - [panel setCanChooseDirectories:YES]; - [panel setCanChooseFiles:NO]; + [panel setCanChooseDirectories:true]; + [panel setCanChooseFiles:false]; [panel setPrompt:@"Select"]; [panel setDirectoryURL:[[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"]]; [panel beginSheetModalForWindow:self completionHandler:^(NSModalResponse result) { @@ -495,12 +674,12 @@ [self.bootROMsFolderItem setTitle:[url lastPathComponent]]; NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:[url path]]; [icon setSize:NSMakeSize(16, 16)]; - [self.bootROMsFolderItem setHidden:NO]; + [self.bootROMsFolderItem setHidden:false]; [self.bootROMsFolderItem setImage:icon]; [self.bootROMsButton selectItemAtIndex:1]; } else { - [self.bootROMsFolderItem setHidden:YES]; + [self.bootROMsFolderItem setHidden:true]; [self.bootROMsButton selectItemAtIndex:0]; } } @@ -644,4 +823,56 @@ } [[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"]; + +} + +- (IBAction)changeFilterScreenshots:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setBool:[(NSButton *)sender state] == NSOnState + forKey:@"GBFilterScreenshots"]; +} + +- (NSButton *)screenshotFilterCheckbox +{ + return _screenshotFilterCheckbox; +} + +- (void)setScreenshotFilterCheckbox:(NSButton *)screenshotFilterCheckbox +{ + _screenshotFilterCheckbox = screenshotFilterCheckbox; + if (![GBViewMetal isSupported]) { + [_screenshotFilterCheckbox setEnabled:false]; + } + else { + [_screenshotFilterCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]]; + } +} + @end diff --git a/Cocoa/GBS.xib b/Cocoa/GBS.xib new file mode 100644 index 0000000..534ff55 --- /dev/null +++ b/Cocoa/GBS.xib @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cocoa/GBSplitView.m b/Cocoa/GBSplitView.m index a56c24e..d24d580 100644 --- a/Cocoa/GBSplitView.m +++ b/Cocoa/GBSplitView.m @@ -8,7 +8,7 @@ - (void)setDividerColor:(NSColor *)color { _dividerColor = color; - [self setNeedsDisplay:YES]; + [self setNeedsDisplay:true]; } - (NSColor *)dividerColor diff --git a/Cocoa/GBTerminalTextFieldCell.h b/Cocoa/GBTerminalTextFieldCell.h index 484e0c3..b760336 100644 --- a/Cocoa/GBTerminalTextFieldCell.h +++ b/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/Cocoa/GBTerminalTextFieldCell.m b/Cocoa/GBTerminalTextFieldCell.m index c1ed203..e1ba957 100644 --- a/Cocoa/GBTerminalTextFieldCell.m +++ b/Cocoa/GBTerminalTextFieldCell.m @@ -17,7 +17,7 @@ return field_editor; } field_editor = [[GBTerminalTextView alloc] init]; - [field_editor setFieldEditor:YES]; + [field_editor setFieldEditor:true]; field_editor.gb = self.gb; return field_editor; } @@ -109,7 +109,7 @@ [self updateReverseSearch]; } else { - [self setNeedsDisplay:YES]; + [self setNeedsDisplay:true]; reverse_search_mode = true; } diff --git a/Cocoa/GBView.h b/Cocoa/GBView.h index 80721cd..01481a7 100644 --- a/Cocoa/GBView.h +++ b/Cocoa/GBView.h @@ -1,6 +1,8 @@ #import #include #import +#import "GBOSDView.h" +@class Document; typedef enum { GB_FRAME_BLENDING_MODE_DISABLED, @@ -13,14 +15,17 @@ 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; - (void)screenSizeChanged; - (void)setRumble: (double)amp; +- (NSImage *)renderToImage; @end diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index 6854ba7..5ae9c79 100644 --- a/Cocoa/GBView.m +++ b/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 @@ -105,9 +106,9 @@ static const uint8_t workboy_vk_to_key[] = { { uint32_t *image_buffers[3]; unsigned char current_buffer; - BOOL mouse_hidden; + bool mouse_hidden; NSTrackingArea *tracking_area; - BOOL _mouseHidingEnabled; + bool _mouseHidingEnabled; bool axisActive[2]; bool underclockKeyDown; double clockMultiplier; @@ -116,6 +117,8 @@ static const uint8_t workboy_vk_to_key[] = { NSEventModifierFlags previousModifiers; JOYController *lastController; GB_frame_blending_mode_t _frameBlendingMode; + bool _turbo; + bool _mouseControlEnabled; } + (instancetype)alloc @@ -141,9 +144,11 @@ 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 + options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved owner:self userInfo:nil]; [self addTrackingArea:tracking_area]; @@ -152,6 +157,7 @@ static const uint8_t workboy_vk_to_key[] = { [self addSubview:self.internalView]; self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; [JOYController registerListener:self]; + _mouseControlEnabled = true; } - (void)screenSizeChanged @@ -179,7 +185,7 @@ static const uint8_t workboy_vk_to_key[] = { - (void) setFrameBlendingMode:(GB_frame_blending_mode_t)frameBlendingMode { _frameBlendingMode = frameBlendingMode; - [self setNeedsDisplay:YES]; + [self setNeedsDisplay:true]; } @@ -256,21 +262,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 +329,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 +341,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 +365,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 +403,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 +415,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 +435,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; } } @@ -390,8 +461,22 @@ static const uint8_t workboy_vk_to_key[] = { [lastController setRumbleAmplitude:amp]; } +- (bool)shouldControllerUseJoystickForMotion:(JOYController *)controller +{ + if (!_gb) return false; + if (!GB_has_accelerometer(_gb)) return false; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7JoystickOverride"]) return true; + for (JOYAxes3D *axes in controller.axes3D) { + if (axes.usage == JOYAxes3DUsageOrientation || axes.usage == JOYAxes3DUsageAcceleration) { + return false; + } + } + return true; +} + - (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis { + if (!_gb) return; if (![self.window isMainWindow]) return; NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; @@ -401,27 +486,60 @@ 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; } } +- (void)controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes +{ + if (!_gb) return; + if ([self shouldControllerUseJoystickForMotion:controller]) { + if (!self.mouseControlsActive) { + GB_set_accelerometer_values(_gb, -axes.value.x, -axes.value.y); + } + } +} + +- (void)controller:(JOYController *)controller movedAxes3D:(JOYAxes3D *)axes +{ + if (!_gb) return; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7JoystickOverride"]) return; + if (self.mouseControlsActive) return; + + if (axes.usage == JOYAxes3DUsageOrientation) { + for (JOYAxes3D *axes in controller.axes3D) { + // Only use orientation if there's no acceleration axes + if (axes.usage == JOYAxes3DUsageAcceleration) { + return; + } + } + JOYPoint3D point = axes.normalizedValue; + GB_set_accelerometer_values(_gb, point.x, point.z); + } + else if (axes.usage == JOYAxes3DUsageAcceleration) { + JOYPoint3D point = axes.gUnitsValue; + GB_set_accelerometer_values(_gb, point.x, point.z); + } +} + - (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button { + if (!_gb) return; if (![self.window isMainWindow]) return; - if (controller != lastController) { - [self setRumble:0]; - lastController = controller; - } - + _mouseControlEnabled = false; + if (button.type == JOYButtonTypeAxes2DEmulated && [self shouldControllerUseJoystickForMotion:controller]) return; unsigned player_count = GB_get_player_count(_gb); + if (self.document.partner) { + player_count = 2; + } IOPMAssertionID assertionID; IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); @@ -435,7 +553,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 +565,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; @@ -483,14 +636,21 @@ static const uint8_t workboy_vk_to_key[] = { - (BOOL)acceptsFirstResponder { - return YES; + return true; +} + +- (bool)mouseControlsActive +{ + return _gb && GB_is_inited(_gb) && GB_has_accelerometer(_gb) && + _mouseControlEnabled && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7AllowMouse"]; } - (void)mouseEntered:(NSEvent *)theEvent { if (!mouse_hidden) { mouse_hidden = true; - if (_mouseHidingEnabled) { + if (_mouseHidingEnabled && + !self.mouseControlsActive) { [NSCursor hide]; } } @@ -508,7 +668,47 @@ static const uint8_t workboy_vk_to_key[] = { [super mouseExited:theEvent]; } -- (void)setMouseHidingEnabled:(BOOL)mouseHidingEnabled +- (void)mouseDown:(NSEvent *)event +{ + _mouseControlEnabled = true; + if (self.mouseControlsActive) { + if (event.type == NSEventTypeLeftMouseDown) { + GB_set_key_state(_gb, GB_KEY_A, true); + } + } +} + +- (void)mouseUp:(NSEvent *)event +{ + if (self.mouseControlsActive) { + if (event.type == NSEventTypeLeftMouseUp) { + GB_set_key_state(_gb, GB_KEY_A, false); + } + } +} + +- (void)mouseMoved:(NSEvent *)event +{ + if (self.mouseControlsActive) { + NSPoint point = [self convertPoint:[event locationInWindow] toView:nil]; + + point.x /= self.frame.size.width; + point.x *= 2; + point.x -= 1; + + point.y /= self.frame.size.height; + point.y *= 2; + point.y -= 1; + + if (GB_get_screen_width(_gb) != 160) { // has border + point.x *= 256 / 160.0; + point.y *= 224 / 114.0; + } + GB_set_accelerometer_values(_gb, -point.x, point.y); + } +} + +- (void)setMouseHidingEnabled:(bool)mouseHidingEnabled { if (mouseHidingEnabled == _mouseHidingEnabled) return; @@ -523,7 +723,7 @@ static const uint8_t workboy_vk_to_key[] = { } } -- (BOOL)isMouseHidingEnabled +- (bool)isMouseHidingEnabled { return _mouseHidingEnabled; } @@ -550,4 +750,35 @@ 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_save_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; +} + +- (NSImage *)renderToImage; +{ + /* Not going to support this on OpenGL, OpenGL is too much of a terrible API for me + to bother figuring out how the hell something so trivial can be done. */ + return nil; +} @end diff --git a/Cocoa/GBViewGL.m b/Cocoa/GBViewGL.m index b80973e..dda8584 100644 --- a/Cocoa/GBViewGL.m +++ b/Cocoa/GBViewGL.m @@ -19,7 +19,7 @@ NSOpenGLContext *context = [[NSOpenGLContext alloc] initWithFormat:pf shareContext:nil]; self.internalView = [[GBOpenGLView alloc] initWithFrame:self.frame pixelFormat:pf]; - ((GBOpenGLView *)self.internalView).wantsBestResolutionOpenGLSurface = YES; + ((GBOpenGLView *)self.internalView).wantsBestResolutionOpenGLSurface = true; ((GBOpenGLView *)self.internalView).openGLContext = context; } @@ -27,8 +27,8 @@ { [super flip]; dispatch_async(dispatch_get_main_queue(), ^{ - [self.internalView setNeedsDisplay:YES]; - [self setNeedsDisplay:YES]; + [self.internalView setNeedsDisplay:true]; + [self setNeedsDisplay:true]; }); } diff --git a/Cocoa/GBViewMetal.m b/Cocoa/GBViewMetal.m index 9a1c78b..ae7443f 100644 --- a/Cocoa/GBViewMetal.m +++ b/Cocoa/GBViewMetal.m @@ -1,3 +1,4 @@ +#import #import "GBViewMetal.h" #pragma clang diagnostic ignored "-Wpartial-availability" @@ -51,8 +52,9 @@ static const vector_float2 rect[] = MTKView *view = [[MTKView alloc] initWithFrame:self.frame device:(device = MTLCreateSystemDefaultDevice())]; view.delegate = self; self.internalView = view; - view.paused = YES; - view.enableSetNeedsDisplay = YES; + view.paused = true; + view.enableSetNeedsDisplay = true; + view.framebufferOnly = false; vertices = [device newBufferWithBytes:rect length:sizeof(rect) @@ -92,7 +94,7 @@ static const vector_float2 rect[] = withString:scaler_source]; MTLCompileOptions *options = [[MTLCompileOptions alloc] init]; - options.fastMathEnabled = YES; + options.fastMathEnabled = true; id library = [device newLibraryWithSource:shader_source options:options error:&error]; @@ -123,7 +125,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 +133,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; @@ -208,8 +210,23 @@ static const vector_float2 rect[] = { [super flip]; dispatch_async(dispatch_get_main_queue(), ^{ - [(MTKView *)self.internalView setNeedsDisplay:YES]; + [(MTKView *)self.internalView setNeedsDisplay:true]; }); } +- (NSImage *)renderToImage +{ + CIImage *ciImage = [CIImage imageWithMTLTexture:[[(MTKView *)self.internalView currentDrawable] texture] + options:@{ + kCIImageColorSpace: (__bridge_transfer id)CGColorSpaceCreateDeviceRGB() + }]; + ciImage = [ciImage imageByApplyingTransform:CGAffineTransformTranslate(CGAffineTransformMakeScale(1, -1), + 0, ciImage.extent.size.height)]; + CIContext *context = [CIContext context]; + CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent]; + NSImage *ret = [[NSImage alloc] initWithCGImage:cgImage size:self.internalView.bounds.size]; + CGImageRelease(cgImage); + return ret; +} + @end diff --git a/Cocoa/GBVisualizerView.h b/Cocoa/GBVisualizerView.h new file mode 100644 index 0000000..5ee4638 --- /dev/null +++ b/Cocoa/GBVisualizerView.h @@ -0,0 +1,6 @@ +#import +#include + +@interface GBVisualizerView : NSView +- (void)addSample:(GB_sample_t *)sample; +@end diff --git a/Cocoa/GBVisualizerView.m b/Cocoa/GBVisualizerView.m new file mode 100644 index 0000000..08f6024 --- /dev/null +++ b/Cocoa/GBVisualizerView.m @@ -0,0 +1,71 @@ +#import "GBVisualizerView.h" +#import "GBPaletteEditorController.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 = [GBPaletteEditorController userPalette]; + 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/Cocoa/GBWarningPopover.m b/Cocoa/GBWarningPopover.m index 411e388..74f6444 100644 --- a/Cocoa/GBWarningPopover.m +++ b/Cocoa/GBWarningPopover.m @@ -10,7 +10,7 @@ static GBWarningPopover *lastPopover; lastPopover = [[self alloc] init]; [lastPopover setBehavior:NSPopoverBehaviorApplicationDefined]; - [lastPopover setAnimates:YES]; + [lastPopover setAnimates:true]; lastPopover.contentViewController = [[NSViewController alloc] initWithNibName:@"PopoverView" bundle:nil]; NSTextField *field = (NSTextField *)lastPopover.contentViewController.view; [field setStringValue:contents]; @@ -20,7 +20,7 @@ static GBWarningPopover *lastPopover; [lastPopover setContentSize:textSize]; if (!view.window.isVisible) { - [view.window setIsVisible:YES]; + [view.window setIsVisible:true]; } [lastPopover showRelativeToRect:view.bounds diff --git a/Cocoa/Info.plist b/Cocoa/Info.plist index 44a21f0..5e409c9 100644 --- a/Cocoa/Info.plist +++ b/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/Cocoa/License.html b/Cocoa/License.html index b21cf8d..9846514 100644 --- a/Cocoa/License.html +++ b/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/Cocoa/MainMenu.xib b/Cocoa/MainMenu.xib index 586d872..aafa8aa 100644 --- a/Cocoa/MainMenu.xib +++ b/Cocoa/MainMenu.xib @@ -12,7 +12,11 @@ - + + + + +

@@ -312,12 +316,35 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -373,6 +400,17 @@ + + + + + + + + + + + @@ -418,6 +456,19 @@ + + + + + + + + + + + + + diff --git a/Cocoa/Next.png b/Cocoa/Next.png new file mode 100644 index 0000000..cd9a4c3 Binary files /dev/null and b/Cocoa/Next.png differ diff --git a/Cocoa/Next@2x.png b/Cocoa/Next@2x.png new file mode 100644 index 0000000..1debb1d Binary files /dev/null and b/Cocoa/Next@2x.png differ diff --git a/Cocoa/Pause.png b/Cocoa/Pause.png new file mode 100644 index 0000000..2bb380b Binary files /dev/null and b/Cocoa/Pause.png differ diff --git a/Cocoa/Pause@2x.png b/Cocoa/Pause@2x.png new file mode 100644 index 0000000..36b6da0 Binary files /dev/null and b/Cocoa/Pause@2x.png differ diff --git a/Cocoa/Play.png b/Cocoa/Play.png new file mode 100644 index 0000000..3f87092 Binary files /dev/null and b/Cocoa/Play.png differ diff --git a/Cocoa/Play@2x.png b/Cocoa/Play@2x.png new file mode 100644 index 0000000..0de0553 Binary files /dev/null and b/Cocoa/Play@2x.png differ diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index 99c6543..dcaa161 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -13,6 +13,9 @@ + + + @@ -21,10 +24,10 @@ - + - + @@ -49,17 +52,25 @@ + + + + + + + + @@ -73,22 +84,31 @@ + + + + + + + + + - + - - + + @@ -96,8 +116,8 @@ - - + + @@ -132,9 +152,20 @@ + - - + + @@ -142,8 +173,8 @@ - - + + @@ -156,6 +187,7 @@ +
@@ -163,9 +195,26 @@
+ + + + + + + + + + + + + + + + + - - + + @@ -173,8 +222,8 @@ - - + + @@ -193,8 +242,8 @@ - - + + @@ -202,8 +251,8 @@ - - + + @@ -212,9 +261,9 @@ - - - + + + @@ -223,8 +272,8 @@ - - + + @@ -232,8 +281,8 @@ - - + + @@ -252,8 +301,8 @@ + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -335,105 +488,57 @@ - - - - - - - - - - - - - + + + + - + - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + - - + + @@ -452,23 +557,40 @@ - - + + + + + + + + + + + + + + + + + + + - + - + - + @@ -476,24 +598,24 @@ - - - - + + + + - - + + - + - + @@ -512,7 +634,7 @@ - + @@ -544,8 +666,8 @@ - - + + @@ -553,8 +675,8 @@ - - + + @@ -569,11 +691,11 @@ - - + + - + @@ -582,7 +704,7 @@ - + @@ -600,9 +722,51 @@ + + + + + + + + + + + + - - + + @@ -618,32 +782,10 @@ - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cocoa/Previous.png b/Cocoa/Previous.png new file mode 100644 index 0000000..cc91221 Binary files /dev/null and b/Cocoa/Previous.png differ diff --git a/Cocoa/Previous@2x.png b/Cocoa/Previous@2x.png new file mode 100644 index 0000000..77b0157 Binary files /dev/null and b/Cocoa/Previous@2x.png differ diff --git a/Cocoa/Rewind.png b/Cocoa/Rewind.png new file mode 100644 index 0000000..999f358 Binary files /dev/null and b/Cocoa/Rewind.png differ diff --git a/Cocoa/Rewind@2x.png b/Cocoa/Rewind@2x.png new file mode 100644 index 0000000..d845b54 Binary files /dev/null and b/Cocoa/Rewind@2x.png differ diff --git a/Cocoa/UpdateWindow.xib b/Cocoa/UpdateWindow.xib new file mode 100644 index 0000000..e34f8f2 --- /dev/null +++ b/Cocoa/UpdateWindow.xib @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/apu.c b/Core/apu.c index 7e7ab31..ce775fb 100644 --- a/Core/apu.c +++ b/Core/apu.c @@ -4,9 +4,6 @@ #include #include "gb.h" -#define likely(x) __builtin_expect((x), 1) -#define unlikely(x) __builtin_expect((x), 0) - static const uint8_t duties[] = { 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, @@ -43,6 +40,8 @@ bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index) case GB_NOISE: return gb->io_registers[GB_IO_NR42] & 0xF8; + + nodefault; } return false; @@ -61,12 +60,21 @@ static uint8_t agb_bias_for_channel(GB_gameboy_t *gb, unsigned index) return 0; case GB_NOISE: return gb->apu.noise_channel.current_volume; + + nodefault; } return 0; } static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsigned cycles_offset) { + if (gb->model >= GB_MODEL_AGB && index == GB_WAVE) { + /* For some reason, channel 3 is inverted on the AGB */ + value ^= 0xF; + } + + if (value == 0 && gb->apu.samples[index] == 0) return; + if (gb->model >= GB_MODEL_AGB) { /* On the AGB, because no analog mixing is done, the behavior of NR51 is a bit different. A channel that is not connected to a terminal is idenitcal to a connected channel @@ -77,11 +85,6 @@ static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsign unsigned right_volume = (gb->io_registers[GB_IO_NR50] & 7) + 1; unsigned left_volume = ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1; - if (index == GB_WAVE) { - /* For some reason, channel 3 is inverted on the AGB */ - value ^= 0xF; - } - GB_sample_t output; uint8_t bias = agb_bias_for_channel(gb, index); @@ -137,12 +140,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 +242,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,22 +266,24 @@ 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; + if (gb->apu.square_channels[index].sample_surpressed) return; uint8_t duty = gb->io_registers[index == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; update_sample(gb, index, @@ -250,62 +292,124 @@ static void update_square_sample(GB_gameboy_t *gb, unsigned index) 0); } - -/* the effects of NRX2 writes on current volume are not well documented and differ - between models and variants. The exact behavior can only be verified on CGB as it - 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 inline void update_wave_sample(GB_gameboy_t *gb, unsigned cycles) { - if (value & 8) { - (*volume)++; + if (gb->apu.wave_channel.current_sample_index & 1) { + update_sample(gb, GB_WAVE, + (gb->apu.wave_channel.current_sample_byte & 0xF) >> gb->apu.wave_channel.shift, + cycles); } - - if (((value ^ old_value) & 8)) { - *volume = 0x10 - *volume; + else { + update_sample(gb, GB_WAVE, + (gb->apu.wave_channel.current_sample_byte >> 4) >> gb->apu.wave_channel.shift, + cycles); } +} - if ((value & 7) && !(old_value & 7) && *volume && !(value & 8)) { - (*volume)--; +static void _nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value, uint8_t *countdown, GB_envelope_clock_t *lock) +{ + if (lock->clock) { + *countdown = value & 7; } - - if ((old_value & 7) && (value & 8)) { - (*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 (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 (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_channels[index].envelope_clock.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_channels[index].envelope_clock.locked = true; + } + } + else { + if (gb->apu.square_channels[index].current_volume > 0) { + gb->apu.square_channels[index].current_volume--; + } + else { + gb->apu.square_channels[index].envelope_clock.locked = true; + } + } + + if (gb->apu.is_active[index]) { + update_square_sample(gb, index); } } @@ -313,33 +417,65 @@ 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_channel.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_channel.envelope_clock.locked = true; + } + } + else { + if (gb->apu.noise_channel.current_volume > 0) { + gb->apu.noise_channel.current_volume--; + } + else { + gb->apu.noise_channel.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; } } void GB_apu_div_event(GB_gameboy_t *gb) { + GB_apu_run(gb, true); if (!gb->apu.global_enable) return; if (gb->apu.skip_div_event == GB_SKIP_DIV_EVENT_SKIP) { gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_SKIPPED; @@ -352,29 +488,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_channels[i].envelope_clock.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_channel.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_channels[i].envelope_clock.clock) { tick_square_envelope(gb, i); + gb->apu.square_channels[i].envelope_clock.clock = false; } - - tick_noise_envelope(gb); } - + + if (gb->apu.noise_channel.envelope_clock.clock) { + tick_noise_envelope(gb); + gb->apu.noise_channel.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) { @@ -388,6 +528,16 @@ void GB_apu_div_event(GB_gameboy_t *gb) if (gb->apu.wave_channel.length_enabled) { if (gb->apu.wave_channel.pulse_length) { if (!--gb->apu.wave_channel.pulse_length) { + if (gb->apu.is_active[GB_WAVE] && gb->model == GB_MODEL_AGB) { + if (gb->apu.wave_channel.sample_countdown == 0) { + gb->apu.wave_channel.current_sample_byte = + gb->io_registers[GB_IO_WAV_START + (((gb->apu.wave_channel.current_sample_index + 1) & 0xF) >> 1)]; + } + else if (gb->apu.wave_channel.sample_countdown == 9) { + // TODO: wtf? + gb->apu.wave_channel.current_sample_byte = gb->io_registers[GB_IO_WAV_START]; + } + } gb->apu.is_active[GB_WAVE] = false; update_sample(gb, GB_WAVE, 0, 0); } @@ -405,68 +555,147 @@ 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_run(GB_gameboy_t *gb) +void GB_apu_div_secondary_event(GB_gameboy_t *gb) { + GB_apu_run(gb, true); + if (!gb->apu.global_enable) return; + 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_channels[i].envelope_clock.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_channel.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.noise_channel.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; + if (gb->apu.is_active[GB_NOISE]) { + update_sample(gb, GB_NOISE, + gb->apu.noise_channel.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + cycles_offset); + } +} + +void GB_apu_run(GB_gameboy_t *gb, bool force) +{ + uint32_t clock_rate = GB_get_clock_rate(gb) * 2; + if (!force || + (gb->apu.apu_cycles > 0x1000) || + (gb->apu_output.sample_cycles >= clock_rate) || + (gb->apu.square_sweep_calculate_countdown || gb->apu.channel_1_restart_hold) || + (gb->model < GB_MODEL_AGB && (gb->apu.wave_channel.bugged_read_countdown || (gb->apu.wave_channel.enable && gb->apu.wave_channel.pulsed)))) { + force = true; + } + if (!force) { + return; + } /* Convert 4MHZ to 2MHz. apu_cycles is always divisable by 4. */ - uint8_t cycles = gb->apu.apu_cycles >> 2; + uint16_t cycles = gb->apu.apu_cycles >> 2; gb->apu.apu_cycles = 0; if (!cycles) return; - + + if (unlikely(gb->apu.wave_channel.bugged_read_countdown)) { + uint16_t cycles_left = cycles; + while (cycles_left) { + cycles_left--; + if (--gb->apu.wave_channel.bugged_read_countdown == 0) { + gb->apu.wave_channel.current_sample_byte = + gb->io_registers[GB_IO_WAV_START + (gb->address_bus & 0xF)]; + if (gb->apu.is_active[GB_WAVE]) { + update_wave_sample(gb, 0); + } + break; + } + } + } + + bool start_ch4 = false; if (likely(!gb->stopped || GB_is_cgb(gb))) { + if (gb->apu.noise_channel.dmg_delayed_start) { + if (gb->apu.noise_channel.dmg_delayed_start == cycles) { + gb->apu.noise_channel.dmg_delayed_start = 0; + start_ch4 = true; + } + else if (gb->apu.noise_channel.dmg_delayed_start > cycles) { + gb->apu.noise_channel.dmg_delayed_start -= cycles; + } + else { + /* Split it into two */ + cycles -= gb->apu.noise_channel.dmg_delayed_start; + gb->apu.apu_cycles = gb->apu.noise_channel.dmg_delayed_start * 4; + GB_apu_run(gb, true); + } + } /* 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; + uint16_t cycles_left = cycles; while (unlikely(cycles_left > gb->apu.square_channels[i].sample_countdown)) { cycles_left -= gb->apu.square_channels[i].sample_countdown + 1; gb->apu.square_channels[i].sample_countdown = (gb->apu.square_channels[i].sample_length ^ 0x7FF) * 2 + 1; gb->apu.square_channels[i].current_sample_index++; gb->apu.square_channels[i].current_sample_index &= 0x7; + gb->apu.square_channels[i].sample_surpressed = false; if (cycles_left == 0 && gb->apu.samples[i] == 0) { gb->apu.pcm_mask[0] &= i == GB_SQUARE_1? 0xF0 : 0x0F; } @@ -481,17 +710,15 @@ void GB_apu_run(GB_gameboy_t *gb) gb->apu.wave_channel.wave_form_just_read = false; if (gb->apu.is_active[GB_WAVE]) { - uint8_t cycles_left = cycles; + uint16_t cycles_left = cycles; while (unlikely(cycles_left > gb->apu.wave_channel.sample_countdown)) { cycles_left -= gb->apu.wave_channel.sample_countdown + 1; gb->apu.wave_channel.sample_countdown = gb->apu.wave_channel.sample_length ^ 0x7FF; gb->apu.wave_channel.current_sample_index++; gb->apu.wave_channel.current_sample_index &= 0x1F; - gb->apu.wave_channel.current_sample = - gb->apu.wave_channel.wave_form[gb->apu.wave_channel.current_sample_index]; - update_sample(gb, GB_WAVE, - gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, - cycles - cycles_left); + gb->apu.wave_channel.current_sample_byte = + gb->io_registers[GB_IO_WAV_START + (gb->apu.wave_channel.current_sample_index >> 1)]; + update_wave_sample(gb, cycles - cycles_left); gb->apu.wave_channel.wave_form_just_read = true; } if (cycles_left) { @@ -499,39 +726,59 @@ void GB_apu_run(GB_gameboy_t *gb) gb->apu.wave_channel.wave_form_just_read = false; } } - - if (gb->apu.is_active[GB_NOISE]) { - 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; - - /* 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; + else if (gb->apu.wave_channel.enable && gb->apu.wave_channel.pulsed && gb->model < GB_MODEL_AGB) { + uint16_t cycles_left = cycles; + while (unlikely(cycles_left > gb->apu.wave_channel.sample_countdown)) { + cycles_left -= gb->apu.wave_channel.sample_countdown + 1; + gb->apu.wave_channel.sample_countdown = gb->apu.wave_channel.sample_length ^ 0x7FF; + if (cycles_left) { + gb->apu.wave_channel.current_sample_byte = + gb->io_registers[GB_IO_WAV_START + (gb->address_bus & 0xF)]; } else { - /* This code is not redundent, it's relevant when switching LFSR widths */ - gb->apu.noise_channel.lfsr &= ~high_bit_mask; + gb->apu.wave_channel.bugged_read_countdown = 1; } - - 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.wave_channel.sample_countdown -= cycles_left; + } + if (gb->apu.wave_channel.sample_countdown == 0) { + gb->apu.wave_channel.bugged_read_countdown = 2; + } + } + + // The noise channel can step even if inactive on the DMG + if (gb->apu.is_active[GB_NOISE] || !GB_is_cgb(gb)) { + uint16_t cycles_left = cycles; + 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; + } + // This while doesn't get an unlikely because the noise channel steps frequently enough + while (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.noise_channel.delta; + gb->apu.noise_channel.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 */ + 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); + } + } + if (cycles_left) { + gb->apu.noise_channel.counter_countdown -= cycles_left; + gb->apu.noise_channel.countdown_reloaded = false; + } + else { + gb->apu.noise_channel.countdown_reloaded = true; } } } @@ -539,21 +786,21 @@ void GB_apu_run(GB_gameboy_t *gb) if (gb->apu_output.sample_rate) { gb->apu_output.cycles_since_render += cycles; - if (gb->apu_output.sample_cycles >= gb->apu_output.cycles_per_sample) { - gb->apu_output.sample_cycles -= gb->apu_output.cycles_per_sample; + if (gb->apu_output.sample_cycles >= clock_rate) { + gb->apu_output.sample_cycles -= clock_rate; 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)); - /* Restore the wave form */ - for (unsigned reg = GB_IO_WAV_START; reg <= GB_IO_WAV_END; reg++) { - gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2] = gb->io_registers[reg] >> 4; - gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = gb->io_registers[reg] & 0xF; - } gb->apu.lf_div = 1; + gb->apu.wave_channel.shift = 4; /* APU glitch: When turning the APU on while DIV's bit 4 (or 5 in double speed mode) is on, the first DIV/APU event is skipped. */ if (gb->div_counter & (gb->cgb_double_speed? 0x2000 : 0x1000)) { @@ -564,6 +811,7 @@ void GB_apu_init(GB_gameboy_t *gb) uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg) { + GB_apu_run(gb, true); if (reg == GB_IO_NR52) { uint8_t value = 0; for (unsigned i = 0; i < GB_N_CHANNELS; i++) { @@ -596,14 +844,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. */ + 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; + case GB_MODEL_DMG_B: + case GB_MODEL_MGB: + 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; + 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; + 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) { + GB_apu_run(gb, true); if (!gb->apu.global_enable && reg != GB_IO_NR52 && reg < GB_IO_WAV_START && (GB_is_cgb(gb) || ( reg != GB_IO_NR11 && @@ -616,7 +987,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END && gb->apu.is_active[GB_WAVE]) { - if (!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) { + if ((!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) || gb->model == GB_MODEL_AGB) { return; } reg = GB_IO_WAV_START + gb->apu.wave_channel.current_sample_index / 2; @@ -636,11 +1007,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 +1023,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 +1061,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 +1068,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_channels[index].envelope_clock); update_square_sample(gb, index); } @@ -724,40 +1088,58 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR14: case GB_IO_NR24: { 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. */ if ((value & 0x80) == 0 && gb->apu.is_active[index]) { /* On an AGB, as well as on CGB C and earlier (TODO: Tested: 0, B and C), it behaves slightly different on double speed. */ - if (gb->model == GB_MODEL_CGB_E /* || gb->model == GB_MODEL_CGB_D */ || gb->apu.square_channels[index].sample_countdown & 1) { + if (gb->model == GB_MODEL_CGB_E || gb->model == GB_MODEL_CGB_D || gb->apu.square_channels[index].sample_countdown & 1) { if (gb->apu.square_channels[index].sample_countdown >> 1 == (gb->apu.square_channels[index].sample_length ^ 0x7FF)) { gb->apu.square_channels[index].current_sample_index--; gb->apu.square_channels[index].current_sample_index &= 7; + gb->apu.square_channels[index].sample_surpressed = false; } } } + 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_channels[index].envelope_clock.locked = false; + gb->apu.square_channels[index].envelope_clock.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.square_channels[index].sample_surpressed = false; + } + /* 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].sample_surpressed)) { + 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. */ @@ -770,8 +1152,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) if ((gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] & 0xF8) != 0 && !gb->apu.is_active[index]) { gb->apu.is_active[index] = true; update_sample(gb, index, 0, 0); - /* We use the highest bit in current_sample_index to mark this sample is not actually playing yet, */ - gb->apu.square_channels[index].current_sample_index |= 0x80; + gb->apu.square_channels[index].sample_surpressed = true; } if (gb->apu.square_channels[index].pulse_length == 0) { gb->apu.square_channels[index].pulse_length = 0x40; @@ -779,23 +1160,40 @@ 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) && gb->model != GB_MODEL_CGB_D) * 2; + /* + if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { + // TODO: This if makes channel_1_sweep_restart_2 fail on CGB-C mode + 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. */ - if ((value & 0x40) && + if (((value & 0x40) || (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_B)) && // Current value is irrelevant on CGB-B and older !gb->apu.square_channels[index].length_enabled && (gb->apu.div_divider & 1) && gb->apu.square_channels[index].pulse_length) { @@ -818,6 +1216,16 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR30: gb->apu.wave_channel.enable = value & 0x80; if (!gb->apu.wave_channel.enable) { + gb->apu.wave_channel.pulsed = false; + if (gb->apu.is_active[GB_WAVE]) { + // Todo: I assume this happens on pre-CGB models; test this with an audible test + if (gb->apu.wave_channel.sample_countdown == 0 && gb->model < GB_MODEL_AGB) { + gb->apu.wave_channel.current_sample_byte = gb->io_registers[GB_IO_WAV_START + (gb->pc & 0xF)]; + } + else if (gb->apu.wave_channel.wave_form_just_read && gb->model <= GB_MODEL_CGB_C) { + gb->apu.wave_channel.current_sample_byte = gb->io_registers[GB_IO_WAV_START + (GB_IO_NR30 & 0xF)]; + } + } gb->apu.is_active[GB_WAVE] = false; update_sample(gb, GB_WAVE, 0, 0); } @@ -828,7 +1236,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR32: gb->apu.wave_channel.shift = (uint8_t[]){4, 0, 1, 2}[(value >> 5) & 3]; if (gb->apu.is_active[GB_WAVE]) { - update_sample(gb, GB_WAVE, gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, 0); + update_wave_sample(gb, 0); } break; case GB_IO_NR33: @@ -838,44 +1246,45 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR34: gb->apu.wave_channel.sample_length &= 0xFF; gb->apu.wave_channel.sample_length |= (value & 7) << 8; - if ((value & 0x80)) { + if (value & 0x80) { + gb->apu.wave_channel.pulsed = true; /* DMG bug: wave RAM gets corrupted if the channel is retriggerred 1 cycle before the APU reads from it. */ if (!GB_is_cgb(gb) && gb->apu.is_active[GB_WAVE] && - gb->apu.wave_channel.sample_countdown == 0 && - gb->apu.wave_channel.enable) { + gb->apu.wave_channel.sample_countdown == 0) { unsigned offset = ((gb->apu.wave_channel.current_sample_index + 1) >> 1) & 0xF; /* This glitch varies between models and even specific instances: DMG-B: Most of them behave as emulated. A few behave differently. SGB: As far as I know, all tested instances behave as emulated. MGB, SGB2: Most instances behave non-deterministically, a few behave as emulated. + + For DMG-B emulation I emulate the most common behavior, which blargg's tests expect (not my own DMG-B, which fails it) + For MGB emulation, I emulate my Game Boy Light, which happens to be deterministic. Additionally, I believe DMGs, including those we behave differently than emulated, are all deterministic. */ - if (offset < 4) { + if (offset < 4 && gb->model != GB_MODEL_MGB) { gb->io_registers[GB_IO_WAV_START] = gb->io_registers[GB_IO_WAV_START + offset]; - gb->apu.wave_channel.wave_form[0] = gb->apu.wave_channel.wave_form[offset / 2]; - gb->apu.wave_channel.wave_form[1] = gb->apu.wave_channel.wave_form[offset / 2 + 1]; } else { memcpy(gb->io_registers + GB_IO_WAV_START, gb->io_registers + GB_IO_WAV_START + (offset & ~3), 4); - memcpy(gb->apu.wave_channel.wave_form, - gb->apu.wave_channel.wave_form + (offset & ~3) * 2, - 8); } } - if (!gb->apu.is_active[GB_WAVE]) { + gb->apu.wave_channel.current_sample_index = 0; + if (gb->apu.is_active[GB_WAVE] && gb->apu.wave_channel.sample_countdown == 0) { + gb->apu.wave_channel.current_sample_byte = gb->io_registers[GB_IO_WAV_START]; + } + if (gb->apu.wave_channel.enable) { gb->apu.is_active[GB_WAVE] = true; update_sample(gb, GB_WAVE, - gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, + (gb->apu.wave_channel.current_sample_byte >> 4) >> gb->apu.wave_channel.shift, 0); } gb->apu.wave_channel.sample_countdown = (gb->apu.wave_channel.sample_length ^ 0x7FF) + 3; - gb->apu.wave_channel.current_sample_index = 0; if (gb->apu.wave_channel.pulse_length == 0) { gb->apu.wave_channel.pulse_length = 0x100; gb->apu.wave_channel.length_enabled = false; @@ -884,7 +1293,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ - if ((value & 0x40) && + if (((value & 0x40) || (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_B)) && // Current value is irrelevant on CGB-B and older !gb->apu.wave_channel.length_enabled && (gb->apu.div_divider & 1) && gb->apu.wave_channel.pulse_length) { @@ -900,10 +1309,6 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } } gb->apu.wave_channel.length_enabled = value & 0x40; - if (gb->apu.is_active[GB_WAVE] && !gb->apu.wave_channel.enable) { - gb->apu.is_active[GB_WAVE] = false; - update_sample(gb, GB_WAVE, 0, 0); - } break; @@ -915,10 +1320,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,9 +1327,11 @@ 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_channel.envelope_clock); update_sample(gb, GB_NOISE, - gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_lfsr_sample ? gb->apu.noise_channel.current_volume : 0, 0); } @@ -937,58 +1340,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.noise_channel.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.noise_channel.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_channel.envelope_clock.locked = false; + gb->apu.noise_channel.envelope_clock.clock = false; + if (!GB_is_cgb(gb) && (gb->apu.noise_channel.alignment & 3) != 0) { + gb->apu.noise_channel.dmg_delayed_start = 6; + } + else { + unsigned divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 2; + if (!divisor) divisor = 2; + gb->apu.noise_channel.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.noise_channel.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.noise_channel.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + 0); + } + gb->apu.noise_channel.lfsr = 0; + gb->apu.noise_channel.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; + } } } @@ -1011,12 +1474,6 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.noise_channel.length_enabled = value & 0x40; break; } - - default: - if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END) { - gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2] = value >> 4; - gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = value & 0xF; - } } gb->io_registers[reg] = value; } @@ -1028,8 +1485,6 @@ void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate) if (sample_rate) { gb->apu_output.highpass_rate = pow(0.999958, GB_get_clock_rate(gb) / (double)sample_rate); } - gb->apu_output.rate_set_in_clocks = false; - GB_apu_update_cycles_per_sample(gb); } void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample) @@ -1039,10 +1494,8 @@ void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample) GB_set_sample_rate(gb, 0); return; } - gb->apu_output.cycles_per_sample = cycles_per_sample; gb->apu_output.sample_rate = GB_get_clock_rate(gb) / cycles_per_sample * 2; gb->apu_output.highpass_rate = pow(0.999958, cycles_per_sample); - gb->apu_output.rate_set_in_clocks = true; } void GB_apu_set_sample_callback(GB_gameboy_t *gb, GB_sample_callback_t callback) @@ -1055,10 +1508,7 @@ void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode) gb->apu_output.highpass_mode = mode; } -void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb) +void GB_set_interference_volume(GB_gameboy_t *gb, double volume) { - if (gb->apu_output.rate_set_in_clocks) return; - if (gb->apu_output.sample_rate) { - gb->apu_output.cycles_per_sample = 2 * GB_get_clock_rate(gb) / (double)gb->apu_output.sample_rate; /* 2 * because we use 8MHz units */ - } + gb->apu_output.interference_volume = volume; } diff --git a/Core/apu.h b/Core/apu.h index a3a36a6..729ccce 100644 --- a/Core/apu.h +++ b/Core/apu.h @@ -3,8 +3,7 @@ #include #include #include -#include "gb_struct_def.h" - +#include "defs.h" #ifdef GB_INTERNAL /* Speed = 1 / Length (in seconds) */ @@ -46,12 +45,19 @@ 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 { bool global_enable; - uint8_t apu_cycles; + uint16_t apu_cycles; uint8_t samples[GB_N_CHANNELS]; bool is_active[GB_N_CHANNELS]; @@ -64,23 +70,24 @@ 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; + + uint8_t channel_1_restart_hold; + uint16_t channel1_completed_addend; struct { uint16_t pulse_length; // Reloaded from NRX1 (xorred), in 256Hz DIV ticks uint8_t current_volume; // Reloaded from NRX2 uint8_t volume_countdown; // Reloaded from NRX2 - uint8_t current_sample_index; /* For save state compatibility, - highest bit is reused (See NR14/NR24's - write code)*/ + uint8_t current_sample_index; + bool sample_surpressed; uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length, xorred $7FF) uint16_t sample_length; // From NRX3, NRX4, in APU ticks bool length_enabled; // NRX4 - + GB_envelope_clock_t envelope_clock; } square_channels[2]; struct { @@ -92,10 +99,10 @@ typedef struct uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length, xorred $7FF) uint8_t current_sample_index; - uint8_t current_sample; // Current sample before shifting. - - int8_t wave_form[32]; + uint8_t current_sample_byte; // Current sample byte. bool wave_form_just_read; + bool pulsed; + uint8_t bugged_read_countdown; } wave_channel; struct { @@ -105,20 +112,24 @@ 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) + 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 // 1MHz. This variable keeps track of the alignment. - + bool current_lfsr_sample; + int8_t delta; + bool countdown_reloaded; + uint8_t dmg_delayed_start; + GB_envelope_clock_t envelope_clock; } noise_channel; -#define GB_SKIP_DIV_EVENT_INACTIVE 0 -#define GB_SKIP_DIV_EVENT_SKIPPED 1 -#define GB_SKIP_DIV_EVENT_SKIP 2 - uint8_t skip_div_event; - bool current_lfsr_sample; + enum { + GB_SKIP_DIV_EVENT_INACTIVE, + GB_SKIP_DIV_EVENT_SKIPPED, + GB_SKIP_DIV_EVENT_SKIP, + } skip_div_event:8; uint8_t pcm_mask[2]; // For CGB-0 to CGB-C PCM read glitch } GB_apu_t; @@ -132,8 +143,7 @@ typedef enum { typedef struct { unsigned sample_rate; - double sample_cycles; // In 8 MHz units - double cycles_per_sample; + unsigned sample_cycles; // Counts by sample_rate until it reaches the clock frequency // Samples are NOT normalized to MAX_CH_AMP * 4 at this stage! unsigned cycles_since_render; @@ -148,22 +158,24 @@ 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_init(GB_gameboy_t *gb); -void GB_apu_run(GB_gameboy_t *gb); -void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb); -void GB_borrow_sgb_border(GB_gameboy_t *gb); +internal bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index); +internal void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value); +internal uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg); +internal void GB_apu_div_event(GB_gameboy_t *gb); +internal void GB_apu_div_secondary_event(GB_gameboy_t *gb); +internal void GB_apu_init(GB_gameboy_t *gb); +internal void GB_apu_run(GB_gameboy_t *gb, bool force); #endif #endif /* apu_h */ diff --git a/Core/camera.c b/Core/camera.c index bef8489..7751f18 100644 --- a/Core/camera.c +++ b/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/Core/camera.h b/Core/camera.h index 21c69b6..1461f3a 100644 --- a/Core/camera.h +++ b/Core/camera.h @@ -1,7 +1,7 @@ #ifndef camera_h #define camera_h #include -#include "gb_struct_def.h" +#include "defs.h" typedef uint8_t (*GB_camera_get_pixel_callback_t)(GB_gameboy_t *gb, uint8_t x, uint8_t y); typedef void (*GB_camera_update_request_callback_t)(GB_gameboy_t *gb); diff --git a/Core/cheats.c b/Core/cheats.c index 14875e0..8b5a7a0 100644 --- a/Core/cheats.c +++ b/Core/cheats.c @@ -32,10 +32,11 @@ static uint16_t bank_for_addr(GB_gameboy_t *gb, uint16_t addr) void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value) { - if (!gb->cheat_enabled) return; - if (!gb->boot_rom_finished) return; + if (likely(!gb->cheat_enabled)) return; + if (likely(gb->cheat_count == 0)) return; // Optimization + if (unlikely(!gb->boot_rom_finished)) return; const GB_cheat_hash_t *hash = gb->cheat_hash[hash_addr(address)]; - if (hash) { + if (unlikely(hash)) { for (unsigned i = 0; i < hash->size; i++) { GB_cheat_t *cheat = hash->cheats[i]; if (cheat->address == address && cheat->enabled && (!cheat->use_old_value || cheat->old_value == *value)) { @@ -69,7 +70,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 +101,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 +110,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 +201,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 +223,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; } } @@ -250,7 +251,7 @@ void GB_load_cheats(GB_gameboy_t *gb, const char *path) uint32_t struct_size = 0; fread(&magic, sizeof(magic), 1, f); fread(&struct_size, sizeof(struct_size), 1, f); - if (magic != CHEAT_MAGIC && magic != __builtin_bswap32(CHEAT_MAGIC)) { + if (magic != LE32(CHEAT_MAGIC) && magic != BE32(CHEAT_MAGIC)) { GB_log(gb, "The file is not a SameBoy cheat database"); return; } @@ -267,7 +268,7 @@ void GB_load_cheats(GB_gameboy_t *gb, const char *path) GB_cheat_t cheat; while (fread(&cheat, sizeof(cheat), 1, f)) { - if (magic == __builtin_bswap32(CHEAT_MAGIC)) { + if (magic != CHEAT_MAGIC) { cheat.address = __builtin_bswap16(cheat.address); cheat.bank = __builtin_bswap16(cheat.bank); } diff --git a/Core/cheats.h b/Core/cheats.h index cf8aa20..f9c076c 100644 --- a/Core/cheats.h +++ b/Core/cheats.h @@ -1,6 +1,6 @@ #ifndef cheats_h #define cheats_h -#include "gb_struct_def.h" +#include "defs.h" #define GB_CHEAT_ANY_BANK 0xFFFF @@ -20,7 +20,7 @@ int GB_save_cheats(GB_gameboy_t *gb, const char *path); #ifdef GB_DISABLE_CHEATS #define GB_apply_cheat(...) #else -void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value); +internal void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value); #endif #endif diff --git a/Core/debugger.c b/Core/debugger.c index 002d455..becc475 100644 --- a/Core/debugger.c +++ b/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; @@ -438,23 +428,23 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, if (string[0] != '$' && (string[0] < '0' || string[0] > '9')) { if (length == 1) { switch (string[0]) { - case 'a': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_AF]}; - case 'f': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_AF]}; - case 'b': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_BC]}; - case 'c': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_BC]}; - case 'd': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_DE]}; - case 'e': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_DE]}; - case 'h': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_HL]}; - case 'l': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_HL]}; + case 'a': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->af}; + case 'f': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->af}; + case 'b': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->bc}; + case 'c': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->bc}; + case 'd': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->de}; + case 'e': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->de}; + case 'h': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->hl}; + case 'l': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->hl}; } } else if (length == 2) { switch (string[0]) { - case 'a': if (string[1] == 'f') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_AF]}; - case 'b': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_BC]}; - case 'd': if (string[1] == 'e') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_DE]}; - case 'h': if (string[1] == 'l') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_HL]}; - case 's': if (string[1] == 'p') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_SP]}; + case 'a': if (string[1] == 'f') return (lvalue_t){LVALUE_REG16, .register_address = &gb->af}; + case 'b': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->bc}; + case 'd': if (string[1] == 'e') return (lvalue_t){LVALUE_REG16, .register_address = &gb->de}; + case 'h': if (string[1] == 'l') return (lvalue_t){LVALUE_REG16, .register_address = &gb->hl}; + case 's': if (string[1] == 'p') return (lvalue_t){LVALUE_REG16, .register_address = &gb->sp}; case 'p': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->pc}; } } @@ -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; @@ -616,23 +606,23 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, if (string[0] != '$' && (string[0] < '0' || string[0] > '9')) { if (length == 1) { switch (string[0]) { - case 'a': ret = VALUE_16(gb->registers[GB_REGISTER_AF] >> 8); goto exit; - case 'f': ret = VALUE_16(gb->registers[GB_REGISTER_AF] & 0xFF); goto exit; - case 'b': ret = VALUE_16(gb->registers[GB_REGISTER_BC] >> 8); goto exit; - case 'c': ret = VALUE_16(gb->registers[GB_REGISTER_BC] & 0xFF); goto exit; - case 'd': ret = VALUE_16(gb->registers[GB_REGISTER_DE] >> 8); goto exit; - case 'e': ret = VALUE_16(gb->registers[GB_REGISTER_DE] & 0xFF); goto exit; - case 'h': ret = VALUE_16(gb->registers[GB_REGISTER_HL] >> 8); goto exit; - case 'l': ret = VALUE_16(gb->registers[GB_REGISTER_HL] & 0xFF); goto exit; + case 'a': ret = VALUE_16(gb->af >> 8); goto exit; + case 'f': ret = VALUE_16(gb->af & 0xFF); goto exit; + case 'b': ret = VALUE_16(gb->bc >> 8); goto exit; + case 'c': ret = VALUE_16(gb->bc & 0xFF); goto exit; + case 'd': ret = VALUE_16(gb->de >> 8); goto exit; + case 'e': ret = VALUE_16(gb->de & 0xFF); goto exit; + case 'h': ret = VALUE_16(gb->hl >> 8); goto exit; + case 'l': ret = VALUE_16(gb->hl & 0xFF); goto exit; } } else if (length == 2) { switch (string[0]) { - case 'a': if (string[1] == 'f') {ret = VALUE_16(gb->registers[GB_REGISTER_AF]); goto exit;} - case 'b': if (string[1] == 'c') {ret = VALUE_16(gb->registers[GB_REGISTER_BC]); goto exit;} - case 'd': if (string[1] == 'e') {ret = VALUE_16(gb->registers[GB_REGISTER_DE]); goto exit;} - case 'h': if (string[1] == 'l') {ret = VALUE_16(gb->registers[GB_REGISTER_HL]); goto exit;} - case 's': if (string[1] == 'p') {ret = VALUE_16(gb->registers[GB_REGISTER_SP]); goto exit;} + case 'a': if (string[1] == 'f') {ret = VALUE_16(gb->af); goto exit;} + case 'b': if (string[1] == 'c') {ret = VALUE_16(gb->bc); goto exit;} + case 'd': if (string[1] == 'e') {ret = VALUE_16(gb->de); goto exit;} + case 'h': if (string[1] == 'l') {ret = VALUE_16(gb->hl); goto exit;} + case 's': if (string[1] == 'p') {ret = VALUE_16(gb->sp); goto exit;} case 'p': if (string[1] == 'c') {ret = (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}; goto exit;} } } @@ -821,15 +811,15 @@ static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const } - GB_log(gb, "AF = $%04x (%c%c%c%c)\n", gb->registers[GB_REGISTER_AF], /* AF can't really be an address */ + GB_log(gb, "AF = $%04x (%c%c%c%c)\n", gb->af, /* AF can't really be an address */ (gb->f & GB_CARRY_FLAG)? 'C' : '-', (gb->f & GB_HALF_CARRY_FLAG)? 'H' : '-', (gb->f & GB_SUBTRACT_FLAG)? 'N' : '-', (gb->f & GB_ZERO_FLAG)? 'Z' : '-'); - GB_log(gb, "BC = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_BC], false)); - GB_log(gb, "DE = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_DE], false)); - GB_log(gb, "HL = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_HL], false)); - GB_log(gb, "SP = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_SP], false)); + GB_log(gb, "BC = %s\n", value_to_string(gb, gb->bc, false)); + GB_log(gb, "DE = %s\n", value_to_string(gb, gb->de, false)); + GB_log(gb, "HL = %s\n", value_to_string(gb, gb->hl, false)); + GB_log(gb, "SP = %s\n", value_to_string(gb, gb->sp, false)); GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false)); GB_log(gb, "IME = %s\n", gb->ime? "Enabled" : "Disabled"); return true; @@ -1461,7 +1451,7 @@ static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const de while (count) { GB_log(gb, "%02x:%04x: ", addr.bank, addr.value); for (unsigned i = 0; i < 16 && count; i++) { - GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); + GB_log(gb, "%02x ", GB_safe_read_memory(gb, addr.value + i)); count--; } addr.value += 16; @@ -1474,7 +1464,7 @@ static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const de while (count) { GB_log(gb, "%04x: ", addr.value); for (unsigned i = 0; i < 16 && count; i++) { - GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); + GB_log(gb, "%02x ", GB_safe_read_memory(gb, addr.value + i)); count--; } addr.value += 16; @@ -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"); @@ -1549,6 +1541,7 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg [GB_MBC2] = "MBC2", [GB_MBC3] = "MBC3", [GB_MBC5] = "MBC5", + [GB_MBC7] = "MBC7", [GB_HUC1] = "HUC-1", [GB_HUC3] = "HUC-3", }; @@ -1558,7 +1551,7 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg if (cartridge->has_ram) { GB_log(gb, "Current mapped RAM bank: %x\n", gb->mbc_ram_bank); if (gb->cartridge_type->mbc_type != GB_HUC1) { - GB_log(gb, "RAM is curently %s\n", gb->mbc_ram_enable? "enabled" : "disabled"); + GB_log(gb, "RAM is currently %s\n", gb->mbc_ram_enable? "enabled" : "disabled"); } } if (cartridge->mbc_type == GB_MBC1 && gb->mbc1_wiring == GB_STANDARD_MBC1_WIRING) { @@ -1575,7 +1568,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 +1607,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; } @@ -1641,9 +1639,9 @@ static bool palettes(GB_gameboy_t *gb, char *arguments, char *modifiers, const d } } - GB_log(gb, "Sprites palettes: \n"); + GB_log(gb, "Object palettes: \n"); for (unsigned i = 0; i < 32; i++) { - GB_log(gb, "%04x ", ((uint16_t *)&gb->sprite_palettes_data)[i]); + GB_log(gb, "%04x ", ((uint16_t *)&gb->object_palettes_data)[i]); if (i % 4 == 3) { GB_log(gb, "\n"); } @@ -1661,7 +1659,7 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg } GB_log(gb, "LCDC:\n"); GB_log(gb, " LCD enabled: %s\n",(gb->io_registers[GB_IO_LCDC] & 128)? "Enabled" : "Disabled"); - GB_log(gb, " %s: %s\n", (gb->cgb_mode? "Sprite priority flags" : "Background and Window"), + GB_log(gb, " %s: %s\n", (gb->cgb_mode? "Object priority flags" : "Background and Window"), (gb->io_registers[GB_IO_LCDC] & 1)? "Enabled" : "Disabled"); GB_log(gb, " Objects: %s\n", (gb->io_registers[GB_IO_LCDC] & 2)? "Enabled" : "Disabled"); GB_log(gb, " Object size: %s\n", (gb->io_registers[GB_IO_LCDC] & 4)? "8x16" : "8x8"); @@ -1774,14 +1772,21 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg GB_log(gb, " Duty cycle %s%% (%s), current index %u/8%s\n", duty > 3? "" : (const char *[]){"12.5", " 25", " 50", " 75"}[duty], duty > 3? "" : (const char *[]){"_______-", "-______-", "-____---", "_------_"}[duty], - gb->apu.square_channels[channel].current_sample_index & 0x7f, - gb->apu.square_channels[channel].current_sample_index >> 7 ? " (suppressed)" : ""); + gb->apu.square_channels[channel].current_sample_index, + gb->apu.square_channels[channel].sample_surpressed ? " (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) { @@ -1793,8 +1798,9 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg GB_log(gb, "\nCH3:\n"); GB_log(gb, " Wave:"); - for (uint8_t i = 0; i < 32; i++) { - GB_log(gb, "%s%X", i%4?"":" ", gb->apu.wave_channel.wave_form[i]); + for (uint8_t i = 0; i < 16; i++) { + GB_log(gb, "%s%X", i % 2? "" : " ", gb->io_registers[GB_IO_WAV_START + i] >> 4); + GB_log(gb, "%X", gb->io_registers[GB_IO_WAV_START + i] & 0xF); } GB_log(gb, "\n"); GB_log(gb, " Current position: %u\n", gb->apu.wave_channel.current_sample_index); @@ -1814,10 +1820,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, @@ -1876,11 +1882,14 @@ static bool wave(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug for (int8_t cur_val = 0xf & mask; cur_val >= 0; cur_val -= shift_amount) { for (uint8_t i = 0; i < 32; i++) { - if ((gb->apu.wave_channel.wave_form[i] & mask) == cur_val) { - GB_log(gb, "%X", gb->apu.wave_channel.wave_form[i]); + uint8_t sample = i & 1? + (gb->io_registers[GB_IO_WAV_START + i / 2] & 0xF) : + (gb->io_registers[GB_IO_WAV_START + i / 2] >> 4); + if ((sample & mask) == cur_val) { + GB_log(gb, "%X", sample); } else { - GB_log(gb, "%c", i%4 == 2 ? '-' : ' '); + GB_log(gb, "%c", i % 4 == 2 ? '-' : ' '); } } GB_log(gb, "\n"); @@ -1889,6 +1898,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 +1931,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"}, @@ -2011,7 +2044,7 @@ void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr) gb->debug_stopped = true; } else { - gb->sp_for_call_depth[gb->debug_call_depth] = gb->registers[GB_REGISTER_SP]; + gb->sp_for_call_depth[gb->debug_call_depth] = gb->sp; gb->addr_for_call_depth[gb->debug_call_depth] = gb->pc; } } @@ -2019,7 +2052,7 @@ void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr) if (gb->backtrace_size < sizeof(gb->backtrace_sps) / sizeof(gb->backtrace_sps[0])) { while (gb->backtrace_size) { - if (gb->backtrace_sps[gb->backtrace_size - 1] < gb->registers[GB_REGISTER_SP]) { + if (gb->backtrace_sps[gb->backtrace_size - 1] < gb->sp) { gb->backtrace_size--; } else { @@ -2027,7 +2060,7 @@ void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr) } } - gb->backtrace_sps[gb->backtrace_size] = gb->registers[GB_REGISTER_SP]; + gb->backtrace_sps[gb->backtrace_size] = gb->sp; gb->backtrace_returns[gb->backtrace_size].bank = bank_for_addr(gb, call_addr); gb->backtrace_returns[gb->backtrace_size].addr = call_addr; gb->backtrace_size++; @@ -2048,9 +2081,9 @@ void GB_debugger_ret_hook(GB_gameboy_t *gb) gb->debug_stopped = true; } else { - if (gb->registers[GB_REGISTER_SP] != gb->sp_for_call_depth[gb->debug_call_depth]) { + if (gb->sp != gb->sp_for_call_depth[gb->debug_call_depth]) { GB_log(gb, "Stack leak detected for function %s!\n", value_to_string(gb, gb->addr_for_call_depth[gb->debug_call_depth], true)); - GB_log(gb, "SP is $%04x, should be $%04x.\n", gb->registers[GB_REGISTER_SP], + GB_log(gb, "SP is $%04x, should be $%04x.\n", gb->sp, gb->sp_for_call_depth[gb->debug_call_depth]); gb->debug_stopped = true; } @@ -2058,7 +2091,7 @@ void GB_debugger_ret_hook(GB_gameboy_t *gb) } while (gb->backtrace_size) { - if (gb->backtrace_sps[gb->backtrace_size - 1] <= gb->registers[GB_REGISTER_SP]) { + if (gb->backtrace_sps[gb->backtrace_size - 1] <= gb->sp) { gb->backtrace_size--; } else { @@ -2163,6 +2196,9 @@ bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input) if (!input[0]) { return true; } + + GB_display_sync(gb); + GB_apu_run(gb, true); char *command_string = input; char *arguments = strchr(input, ' '); @@ -2184,7 +2220,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); @@ -2192,7 +2251,6 @@ bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input) } } -/* Returns true if debugger waits for more commands */ char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *context) { char *command_string = input; @@ -2260,6 +2318,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 +2369,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; } @@ -2482,7 +2545,7 @@ static bool is_in_trivial_memory(uint16_t addr) return false; } -typedef uint16_t GB_opcode_address_getter_t(GB_gameboy_t *gb, uint8_t opcode); +typedef uint16_t opcode_address_getter_t(GB_gameboy_t *gb, uint8_t opcode); uint16_t trivial_1(GB_gameboy_t *gb, uint8_t opcode) { @@ -2508,13 +2571,13 @@ static bool condition_code(GB_gameboy_t *gb, uint8_t opcode) { switch ((opcode >> 3) & 0x3) { case 0: - return !(gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + return !(gb->af & GB_ZERO_FLAG); case 1: - return (gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + return (gb->af & GB_ZERO_FLAG); case 2: - return !(gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + return !(gb->af & GB_CARRY_FLAG); case 3: - return (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + return (gb->af & GB_CARRY_FLAG); } return false; @@ -2531,8 +2594,8 @@ static uint16_t jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode) static uint16_t ret(GB_gameboy_t *gb, uint8_t opcode) { - return GB_read_memory(gb, gb->registers[GB_REGISTER_SP]) | - (GB_read_memory(gb, gb->registers[GB_REGISTER_SP] + 1) << 8); + return GB_read_memory(gb, gb->sp) | + (GB_read_memory(gb, gb->sp + 1) << 8); } @@ -2572,7 +2635,7 @@ static uint16_t jp_hl(GB_gameboy_t *gb, uint8_t opcode) return gb->hl; } -static GB_opcode_address_getter_t *opcodes[256] = { +static opcode_address_getter_t *opcodes[256] = { /* X0 X1 X2 X3 X4 X5 X6 X7 */ /* X8 X9 Xa Xb Xc Xd Xe Xf */ trivial_1, trivial_3, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, /* 0X */ @@ -2614,7 +2677,7 @@ static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *add if (!gb->has_jump_to_breakpoints) return JUMP_TO_NONE; if (!is_in_trivial_memory(gb->pc) || !is_in_trivial_memory(gb->pc + 2) || - !is_in_trivial_memory(gb->registers[GB_REGISTER_SP]) || !is_in_trivial_memory(gb->registers[GB_REGISTER_SP] + 1)) { + !is_in_trivial_memory(gb->sp) || !is_in_trivial_memory(gb->sp + 1)) { return JUMP_TO_NONTRIVIAL; } @@ -2650,7 +2713,7 @@ static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *add return JUMP_TO_NONE; } - GB_opcode_address_getter_t *getter = opcodes[opcode]; + opcode_address_getter_t *getter = opcodes[opcode]; if (!getter) { gb->n_watchpoints = n_watchpoints; return JUMP_TO_NONE; diff --git a/Core/debugger.h b/Core/debugger.h index 0678b30..3d77b7a 100644 --- a/Core/debugger.h +++ b/Core/debugger.h @@ -2,7 +2,7 @@ #define debugger_h #include #include -#include "gb_struct_def.h" +#include "defs.h" #include "symbol_hash.h" @@ -17,14 +17,14 @@ #define GB_debugger_add_symbol(gb, bank, address, symbol) ((void)bank, (void)address, (void)symbol) #else -void GB_debugger_run(GB_gameboy_t *gb); -void GB_debugger_handle_async_commands(GB_gameboy_t *gb); -void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr); -void GB_debugger_ret_hook(GB_gameboy_t *gb); -void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value); -void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr); -const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr); -void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol); +internal void GB_debugger_run(GB_gameboy_t *gb); +internal void GB_debugger_handle_async_commands(GB_gameboy_t *gb); +internal void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr); +internal void GB_debugger_ret_hook(GB_gameboy_t *gb); +internal void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +internal void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr); +internal const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr); +internal void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol); #endif /* GB_DISABLE_DEBUGGER */ #endif diff --git a/Core/defs.h b/Core/defs.h new file mode 100644 index 0000000..b512986 --- /dev/null +++ b/Core/defs.h @@ -0,0 +1,46 @@ +#ifndef defs_h +#define defs_h + +#ifdef GB_INTERNAL + +// "Keyword" definitions +#define likely(x) __builtin_expect((bool)(x), 1) +#define unlikely(x) __builtin_expect((bool)(x), 0) + +#define internal __attribute__((visibility("internal"))) + +#if __clang__ +#define unrolled _Pragma("unroll") +#elif __GNUC__ >= 8 +#define unrolled _Pragma("GCC unroll 8") +#else +#define unrolled +#endif + +#define unreachable() __builtin_unreachable(); +#define nodefault default: unreachable() + +#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 + +struct GB_gameboy_s; +typedef struct GB_gameboy_s GB_gameboy_t; +#endif diff --git a/Core/display.c b/Core/display.c index 2eb8c42..5ce0f2b 100644 --- a/Core/display.c +++ b/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)) { @@ -87,7 +85,7 @@ static void fifo_overlay_object_row(GB_fifo_t *fifo, uint8_t lower, uint8_t uppe /* - Each line is 456 cycles. Without scrolling, sprites or a window: + Each line is 456 cycles. Without scrolling, objects or a window: Mode 2 - 80 cycles / OAM Transfer Mode 3 - 172 cycles / Rendering Mode 0 - 204 cycles / HBlank @@ -109,11 +107,13 @@ typedef struct __attribute__((packed)) { uint8_t x; uint8_t tile; uint8_t flags; -} GB_object_t; +} object_t; -static void display_vblank(GB_gameboy_t *gb) +void GB_display_vblank(GB_gameboy_t *gb) { gb->vblank_just_occured = true; + gb->cycles_since_vblank_callback = 0; + gb->lcd_disabled_outside_of_vblank = false; /* TODO: Slow in turbo mode! */ if (GB_is_hle_sgb(gb)) { @@ -128,7 +128,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 || gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON)) { /* 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 +155,31 @@ 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]; + if (gb->model == GB_MODEL_CGB_0) { + index = 1; // CGB 0 was only available in Indigo! + } + else if (gb->model == GB_MODEL_CGB_A) { + index = 0; // CGB 0 was only available in Indigo! + } + 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 +187,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 +219,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 +246,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 +271,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 +300,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,16 +343,24 @@ 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); } void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index) { if (!gb->rgb_encode_callback || !GB_is_cgb(gb)) return; - uint8_t *palette_data = background_palette? gb->background_palettes_data : gb->sprite_palettes_data; + uint8_t *palette_data = background_palette? gb->background_palettes_data : gb->object_palettes_data; uint16_t color = palette_data[index & ~1] | (palette_data[index | 1] << 8); - (background_palette? gb->background_palettes_rgb : gb->sprite_palettes_rgb)[index / 2] = GB_convert_rgb15(gb, color, false); + (background_palette? gb->background_palettes_rgb : gb->object_palettes_rgb)[index / 2] = GB_convert_rgb15(gb, color, false); } void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode) @@ -324,6 +374,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 @@ -398,6 +459,10 @@ void GB_lcd_off(GB_gameboy_t *gb) gb->accessed_oam_row = -1; gb->wy_triggered = false; + + if (unlikely(gb->lcd_line_callback)) { + gb->lcd_line_callback(gb, 0); + } } static void add_object_from_index(GB_gameboy_t *gb, unsigned index) @@ -414,7 +479,7 @@ static void add_object_from_index(GB_gameboy_t *gb, unsigned index) } /* This reverse sorts the visible objects by location and priority */ - GB_object_t *objects = (GB_object_t *) &gb->oam; + object_t *objects = (object_t *) &gb->oam; bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; signed y = objects[index].y - 16; if (y <= gb->current_line && y + (height_16? 16 : 8) > gb->current_line) { @@ -430,10 +495,38 @@ 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, bool *cgb_d_glitch) +{ + /* + 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; + *cgb_d_glitch = false; + + if (gb->io_registers[GB_IO_LCDC] & 0x10) { + if (gb->model != GB_MODEL_CGB_D) { + *should_use = !(gb->current_tile & 0x80); + return gb->current_tile; + } + *cgb_d_glitch = true; + *should_use = false; + gb->io_registers[GB_IO_LCDC] &= ~0x10; + if (gb->fetcher_state == 3) { + *should_use = false; + *cgb_d_glitch = true; + return 0; + } + return 0; + } + return gb->data_for_sel_glitch; +} + + static void render_pixel_if_possible(GB_gameboy_t *gb) { - GB_fifo_item_t *fifo_item = NULL; - GB_fifo_item_t *oam_fifo_item = NULL; + const GB_fifo_item_t *fifo_item = NULL; + const GB_fifo_item_t *oam_fifo_item = NULL; bool draw_oam = false; bool bg_enabled = true, bg_priority = false; @@ -443,7 +536,7 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) if (fifo_size(&gb->oam_fifo)) { oam_fifo_item = fifo_pop(&gb->oam_fifo); - if (oam_fifo_item->pixel && (gb->io_registers[GB_IO_LCDC] & 2)) { + if (oam_fifo_item->pixel && (gb->io_registers[GB_IO_LCDC] & 2) && unlikely(!gb->objects_disabled)) { draw_oam = true; bg_priority |= oam_fifo_item->bg_priority; } @@ -469,6 +562,12 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) bg_enabled = false; } } + + if (unlikely(gb->background_disabled)) { + bg_enabled = false; + static const GB_fifo_item_t empty_item = {0,}; + fifo_item = &empty_item; + } uint8_t icd_pixel = 0; uint32_t *dest = NULL; @@ -521,14 +620,13 @@ 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) { *dest = gb->rgb_encode_callback(gb, 0, 0, 0); } else { - *dest = gb->sprite_palettes_rgb[oam_fifo_item->palette * 4 + pixel]; + *dest = gb->object_palettes_rgb[oam_fifo_item->palette * 4 + pixel]; } } @@ -563,7 +661,7 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) GB_FETCHER_SLEEP, } fetcher_step_t; - fetcher_step_t fetcher_state_machine [8] = { + static const fetcher_step_t fetcher_state_machine [8] = { GB_FETCHER_SLEEP, GB_FETCHER_GET_TILE, GB_FETCHER_SLEEP, @@ -597,7 +695,10 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) x = gb->window_tile_x; } else { - x = ((gb->io_registers[GB_IO_SCX] / 8) + gb->fetcher_x) & 0x1F; + /* TODO: There is some CGB timing error around here. + Adjusting SCX by 7 or less shouldn't have an effect on a CGB, + but SameBoy is affected by a change of both 7 and 6 (but not less). */ + x = ((gb->io_registers[GB_IO_SCX] + gb->position_in_line + 8) / 8) & 0x1F; } if (gb->model > GB_MODEL_CGB_C) { /* This value is cached on the CGB-D and newer, so it cannot be used to mix tiles together */ @@ -621,6 +722,11 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) break; case GB_FETCHER_GET_TILE_DATA_LOWER: { + bool use_glitched = false; + bool cgb_d_glitch = false; + if (gb->tile_sel_glitch) { + gb->current_tile_data[0] = data_for_tile_sel_glitch(gb, &use_glitched, &cgb_d_glitch); + } 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 +744,39 @@ 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; + } + } + if ((gb->io_registers[GB_IO_LCDC] & 0x10) && gb->tile_sel_glitch) { + 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; + } + } + else if (cgb_d_glitch) { + gb->data_for_sel_glitch = gb->vram[gb->current_tile * 0x10 + ((y & 7) ^ y_flip) * 2]; + 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; + bool cgb_d_glitch = false; + if (gb->tile_sel_glitch) { + gb->current_tile_data[1] = data_for_tile_sel_glitch(gb, &use_glitched, &cgb_d_glitch); + } + uint16_t tile_address = 0; uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb); @@ -668,11 +793,25 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) if (gb->current_tile_attributes & 0x40) { 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; + gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2 + 1 - cgb_d_glitch; + 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; + } + } + 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; + } + } + else if (cgb_d_glitch) { + gb->data_for_sel_glitch = gb->vram[gb->current_tile * 0x10 + ((y & 7) ^ y_flip) * 2 + 1]; + if (gb->vram_ppu_blocked) { + gb->data_for_sel_glitch = 0xFF; + } } } if (gb->wx_triggered) { @@ -682,12 +821,6 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) // fallthrough case GB_FETCHER_PUSH: { - if (gb->fetcher_state == 6) { - /* The background map index increase at this specific point. If this state is not reached, - it will simply not increase. */ - gb->fetcher_x++; - gb->fetcher_x &= 0x1f; - } if (gb->fetcher_state < 7) { gb->fetcher_state++; } @@ -704,14 +837,16 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) gb->fetcher_state++; } break; + + nodefault; } } -static uint16_t get_object_line_address(GB_gameboy_t *gb, const GB_object_t *object) +static uint16_t get_object_line_address(GB_gameboy_t *gb, const object_t *object) { /* TODO: what does the PPU read if DMA is active? */ if (gb->oam_ppu_blocked) { - static const GB_object_t blocked = {0xFF, 0xFF, 0xFF, 0xFF}; + static const object_t blocked = {0xFF, 0xFF, 0xFF, 0xFF}; object = &blocked; } @@ -731,29 +866,399 @@ static uint16_t get_object_line_address(GB_gameboy_t *gb, const GB_object_t *obj return line_address; } +static inline uint8_t flip(uint8_t x) +{ + x = (x & 0xF0) >> 4 | (x & 0x0F) << 4; + x = (x & 0xCC) >> 2 | (x & 0x33) << 2; + x = (x & 0xAA) >> 1 | (x & 0x55) << 1; + return x; +} + +static inline void get_tile_data(const GB_gameboy_t *gb, uint8_t tile_x, uint8_t y, uint16_t map, uint8_t *attributes, uint8_t *data0, uint8_t *data1) +{ + uint8_t current_tile = gb->vram[map + (tile_x & 0x1F) + y / 8 * 32]; + *attributes = GB_is_cgb(gb)? gb->vram[0x2000 + map + (tile_x & 0x1F) + y / 8 * 32] : 0; + + uint16_t tile_address = 0; + + /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ + if (gb->io_registers[GB_IO_LCDC] & 0x10) { + tile_address = current_tile * 0x10; + } + else { + tile_address = (int8_t)current_tile * 0x10 + 0x1000; + } + if (*attributes & 8) { + tile_address += 0x2000; + } + uint8_t y_flip = 0; + if (*attributes & 0x40) { + y_flip = 0x7; + } + + *data0 = gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; + *data1 = gb->vram[tile_address + ((y & 7) ^ y_flip) * 2 + 1]; + + if (*attributes & 0x20) { + *data0 = flip(*data0); + *data1 = flip(*data1); + } + +} + +static void render_line(GB_gameboy_t *gb) +{ + if (gb->disable_rendering) return; + if (!gb->screen) return; + if (gb->current_line > 144) return; // Corrupt save state + + struct { + unsigned pixel:2; // Color, 0-3 + unsigned priority:6; // Object priority – 0 in DMG, OAM index in CGB + unsigned palette:3; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) + bool bg_priority:1; // BG priority bit + } _object_buffer[160 + 16]; // allocate extra to avoid per pixel checks + static const uint8_t empty_object_buffer[sizeof(_object_buffer)]; + const typeof(_object_buffer[0]) *object_buffer; + + if (gb->n_visible_objs && !gb->objects_disabled && (gb->io_registers[GB_IO_LCDC] & 2)) { + object_buffer = &_object_buffer[0]; + object_t *objects = (object_t *) &gb->oam; + memset(_object_buffer, 0, sizeof(_object_buffer)); + + while (gb->n_visible_objs) { + unsigned object_index = gb->visible_objs[gb->n_visible_objs - 1]; + unsigned priority = gb->object_priority == GB_OBJECT_PRIORITY_X? 0 : object_index; + const object_t *object = &objects[object_index]; + gb->n_visible_objs--; + + uint16_t line_address = get_object_line_address(gb, object); + uint8_t data0 = gb->vram[line_address]; + uint8_t data1 = gb->vram[line_address + 1]; + if (gb->n_visible_objs == 0) { + gb->data_for_sel_glitch = data1; + } + if (object->flags & 0x20) { + data0 = flip(data0); + data1 = flip(data1); + } + + typeof(_object_buffer[0]) *p = _object_buffer + object->x; + if (object->x >= 168) { + continue; + } + unrolled for (unsigned x = 0; x < 8; x++) { + unsigned pixel = (data0 >> 7) | ((data1 >> 7) << 1); + data0 <<= 1; + data1 <<= 1; + if (pixel && (!p->pixel || priority < p->priority)) { + p->pixel = pixel; + p->priority = priority; + + if (gb->cgb_mode) { + p->palette = object->flags & 0x7; + } + else { + p->palette = (object->flags & 0x10) >> 4; + } + p->bg_priority = object->flags & 0x80; + } + p++; + } + } + } + else { + object_buffer = (const void *)empty_object_buffer; + } + + + uint32_t *restrict p = gb->screen; + typeof(object_buffer[0]) *object_buffer_pointer = object_buffer + 8; + if (gb->border_mode == GB_BORDER_ALWAYS) { + p += (BORDERED_WIDTH - (WIDTH)) / 2 + BORDERED_WIDTH * (BORDERED_HEIGHT - LINES) / 2; + p += BORDERED_WIDTH * gb->current_line; + } + else { + p += WIDTH * gb->current_line; + } + + if (unlikely(gb->background_disabled) || (!gb->cgb_mode && !(gb->io_registers[GB_IO_LCDC] & 1))) { + uint32_t bg = gb->background_palettes_rgb[gb->cgb_mode? 0 : (gb->io_registers[GB_IO_BGP] & 3)]; + for (unsigned i = 160; i--;) { + if (unlikely(object_buffer_pointer->pixel)) { + uint8_t pixel = object_buffer_pointer->pixel; + if (!gb->cgb_mode) { + pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3); + } + *(p++) = gb->object_palettes_rgb[pixel + (object_buffer_pointer->palette & 7) * 4]; + } + else { + *(p++) = bg; + } + object_buffer_pointer++; + } + return; + } + + unsigned pixels = 0; + uint8_t tile_x = gb->io_registers[GB_IO_SCX] / 8; + unsigned fractional_scroll = gb->io_registers[GB_IO_SCX] & 7; + uint16_t map = 0x1800; + if (gb->io_registers[GB_IO_LCDC] & 0x08) { + map = 0x1C00; + } + uint8_t y = gb->current_line + gb->io_registers[GB_IO_SCY]; + uint8_t attributes; + uint8_t data0, data1; + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + +#define DO_PIXEL() \ +uint8_t pixel = (data0 >> 7) | ((data1 >> 7) << 1);\ +data0 <<= 1;\ +data1 <<= 1;\ +\ +if (unlikely(object_buffer_pointer->pixel) && (pixel == 0 || !(object_buffer_pointer->bg_priority || (attributes & 0x80)) || !(gb->io_registers[GB_IO_LCDC] & 1))) {\ + pixel = object_buffer_pointer->pixel;\ + if (!gb->cgb_mode) {\ + pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3);\ + }\ + *(p++) = gb->object_palettes_rgb[pixel + (object_buffer_pointer->palette & 7) * 4];\ +}\ +else {\ + if (!gb->cgb_mode) {\ + pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3);\ + }\ + *(p++) = gb->background_palettes_rgb[pixel + (attributes & 7) * 4];\ +}\ +pixels++;\ +object_buffer_pointer++\ + + // First 1-8 pixels + data0 <<= fractional_scroll; + data1 <<= fractional_scroll; + bool check_window = gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20); + for (unsigned i = fractional_scroll; i < 8; i++) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { +activate_window: + check_window = false; + map = gb->io_registers[GB_IO_LCDC] & 0x40? 0x1C00 : 0x1800; + tile_x = -1; + y = ++gb->window_y; + break; + } + DO_PIXEL(); + } + tile_x++; + + while (pixels < 160 - 8) { + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + for (unsigned i = 0; i < 8; i++) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } + tile_x++; + } + + gb->fetcher_state = (160 - pixels) & 7; + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + while (pixels < 160) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } + tile_x++; + + get_tile_data(gb, tile_x, y, map, &attributes, gb->current_tile_data, gb->current_tile_data + 1); +#undef DO_PIXEL +} + +static void render_line_sgb(GB_gameboy_t *gb) +{ + if (gb->current_line > 144) return; // Corrupt save state + + struct { + unsigned pixel:2; // Color, 0-3 + unsigned palette:1; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) + bool bg_priority:1; // BG priority bit + } _object_buffer[160 + 16]; // allocate extra to avoid per pixel checks + static const uint8_t empty_object_buffer[sizeof(_object_buffer)]; + const typeof(_object_buffer[0]) *object_buffer; + + if (gb->n_visible_objs && !gb->objects_disabled && (gb->io_registers[GB_IO_LCDC] & 2)) { + object_buffer = &_object_buffer[0]; + object_t *objects = (object_t *) &gb->oam; + memset(_object_buffer, 0, sizeof(_object_buffer)); + + while (gb->n_visible_objs) { + const object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; + gb->n_visible_objs--; + + uint16_t line_address = get_object_line_address(gb, object); + uint8_t data0 = gb->vram[line_address]; + uint8_t data1 = gb->vram[line_address + 1]; + if (object->flags & 0x20) { + data0 = flip(data0); + data1 = flip(data1); + } + + typeof(_object_buffer[0]) *p = _object_buffer + object->x; + if (object->x >= 168) { + continue; + } + unrolled for (unsigned x = 0; x < 8; x++) { + unsigned pixel = (data0 >> 7) | ((data1 >> 7) << 1); + data0 <<= 1; + data1 <<= 1; + if (!p->pixel) { + p->pixel = pixel; + p->palette = (object->flags & 0x10) >> 4; + p->bg_priority = object->flags & 0x80; + } + p++; + } + } + } + else { + object_buffer = (const void *)empty_object_buffer; + } + + + uint8_t *restrict p = gb->sgb->screen_buffer; + typeof(object_buffer[0]) *object_buffer_pointer = object_buffer + 8; + p += WIDTH * gb->current_line; + + if (unlikely(gb->background_disabled) || (!gb->cgb_mode && !(gb->io_registers[GB_IO_LCDC] & 1))) { + for (unsigned i = 160; i--;) { + if (unlikely(object_buffer_pointer->pixel)) { + uint8_t pixel = object_buffer_pointer->pixel; + pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3); + *(p++) = pixel; + } + else { + *(p++) = gb->io_registers[GB_IO_BGP] & 3; + } + object_buffer_pointer++; + } + return; + } + + unsigned pixels = 0; + uint8_t tile_x = gb->io_registers[GB_IO_SCX] / 8; + unsigned fractional_scroll = gb->io_registers[GB_IO_SCX] & 7; + uint16_t map = 0x1800; + if (gb->io_registers[GB_IO_LCDC] & 0x08) { + map = 0x1C00; + } + uint8_t y = gb->current_line + gb->io_registers[GB_IO_SCY]; + uint8_t attributes; + uint8_t data0, data1; + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + +#define DO_PIXEL() \ +uint8_t pixel = (data0 >> 7) | ((data1 >> 7) << 1);\ +data0 <<= 1;\ +data1 <<= 1;\ +\ +if (unlikely(object_buffer_pointer->pixel) && (pixel == 0 || !object_buffer_pointer->bg_priority || !(gb->io_registers[GB_IO_LCDC] & 1))) {\ + pixel = object_buffer_pointer->pixel;\ + pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3);\ + *(p++) = pixel;\ +}\ +else {\ + pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3);\ + *(p++) = pixel;\ +}\ +pixels++;\ +object_buffer_pointer++\ + + // First 1-8 pixels + data0 <<= fractional_scroll; + data1 <<= fractional_scroll; + bool check_window = gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20); + for (unsigned i = fractional_scroll; i < 8; i++) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + activate_window: + check_window = false; + map = gb->io_registers[GB_IO_LCDC] & 0x40? 0x1C00 : 0x1800; + tile_x = -1; + y = ++gb->window_y; + break; + } + DO_PIXEL(); + } + tile_x++; + + while (pixels < 160 - 8) { + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + for (unsigned i = 0; i < 8; i++) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } + tile_x++; + } + + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + while (pixels < 160) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } +} + +static inline uint16_t mode3_batching_length(GB_gameboy_t *gb) +{ + if (gb->model & GB_MODEL_NO_SFC_BIT) return 0; + if (gb->hdma_on) return 0; + if (gb->dma_steps_left) return 0; + if (gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20) && (gb->io_registers[GB_IO_WX] < 8 || gb->io_registers[GB_IO_WX] == 166)) { + return 0; + } + + // No objects or window, timing is trivial + if (gb->n_visible_objs == 0 && !(gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20))) return 167 + (gb->io_registers[GB_IO_SCX] & 7); + + if (gb->hdma_on_hblank) return 0; + + // 300 is a bit more than the maximum Mode 3 length + + // No HBlank interrupt + if (!(gb->io_registers[GB_IO_STAT] & 0x8)) return 300; + // No STAT interrupt requested + if (!(gb->interrupt_enable & 2)) return 300; + + + return 0; +} + /* TODO: It seems that the STAT register's mode bits are always "late" by 4 T-cycles. The PPU logic can be greatly simplified if that delay is simply emulated. */ -void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) +void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) { + gb->cycles_since_vblank_callback += cycles / 2; + /* The PPU does not advance while in STOP mode on the DMG */ if (gb->stopped && !GB_is_cgb(gb)) { - gb->cycles_in_stop_mode += cycles; - if (gb->cycles_in_stop_mode >= LCDC_PERIOD) { - gb->cycles_in_stop_mode -= LCDC_PERIOD; - display_vblank(gb); + if (gb->cycles_since_vblank_callback >= LCDC_PERIOD) { + GB_display_vblank(gb); } return; } - GB_object_t *objects = (GB_object_t *) &gb->oam; + object_t *objects = (object_t *) &gb->oam; - GB_STATE_MACHINE(gb, display, cycles, 2) { + GB_BATCHABLE_STATE_MACHINE(gb, display, cycles, 2, !force) { GB_STATE(gb, display, 1); GB_STATE(gb, display, 2); - // GB_STATE(gb, display, 3); - // GB_STATE(gb, display, 4); - // GB_STATE(gb, display, 5); + GB_STATE(gb, display, 3); + GB_STATE(gb, display, 4); + GB_STATE(gb, display, 5); GB_STATE(gb, display, 6); GB_STATE(gb, display, 7); GB_STATE(gb, display, 8); @@ -766,19 +1271,19 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_STATE(gb, display, 15); GB_STATE(gb, display, 16); GB_STATE(gb, display, 17); - // GB_STATE(gb, display, 19); + GB_STATE(gb, display, 19); GB_STATE(gb, display, 20); 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); @@ -794,8 +1299,10 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { while (true) { - GB_SLEEP(gb, display, 1, LCDC_PERIOD); - display_vblank(gb); + if (gb->cycles_since_vblank_callback < LCDC_PERIOD) { + GB_SLEEP(gb, display, 1, LCDC_PERIOD - gb->cycles_since_vblank_callback); + } + GB_display_vblank(gb); gb->cgb_repeated_a_frame = true; } return; @@ -810,13 +1317,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,10 +1368,8 @@ 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; + if (unlikely(gb->lcd_line_callback)) { + gb->lcd_line_callback(gb, gb->current_line); } gb->oam_write_blocked = GB_is_cgb(gb) && !gb->cgb_double_speed; @@ -907,6 +1406,9 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_STAT_update(gb); gb->n_visible_objs = 0; + if (!gb->dma_steps_left && !gb->oam_ppu_blocked) { + GB_BATCHPOINT(gb, display, 5, 80); + } for (gb->oam_search_index = 0; gb->oam_search_index < 40; gb->oam_search_index++) { if (GB_is_cgb(gb)) { add_object_from_index(gb, gb->oam_search_index); @@ -922,7 +1424,6 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) gb->vram_write_blocked = false; gb->cgb_palettes_blocked = false; gb->oam_write_blocked = GB_is_cgb(gb); - GB_STAT_update(gb); } } gb->cycles_for_line = MODE2_LENGTH + 4; @@ -951,6 +1452,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); @@ -960,12 +1466,26 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) gb->position_in_line = - (gb->io_registers[GB_IO_SCX] & 7) - 8; gb->lcd_x = 0; - gb->fetcher_x = 0; - gb->extra_penalty_for_sprite_at_0 = (gb->io_registers[GB_IO_SCX] & 7); - + gb->extra_penalty_for_object_at_0 = MIN((gb->io_registers[GB_IO_SCX] & 7), 5); /* The actual rendering cycle */ gb->fetcher_state = 0; + if ((gb->mode3_batching_length = mode3_batching_length(gb))) { + GB_BATCHPOINT(gb, display, 3, gb->mode3_batching_length); + if (GB_BATCHED_CYCLES(gb, display) >= gb->mode3_batching_length) { + // Successfully batched! + gb->lcd_x = gb->position_in_line = 160; + gb->cycles_for_line += gb->mode3_batching_length; + if (gb->sgb) { + render_line_sgb(gb); + } + else { + render_line(gb); + } + GB_SLEEP(gb, display, 4, gb->mode3_batching_length); + goto skip_slow_mode_3; + } + } while (true) { /* Handle window */ /* TODO: It appears that WX checks if the window begins *next* pixel, not *this* pixel. For this reason, @@ -1033,7 +1553,7 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) } /* Handle objects */ - /* When the sprite enabled bit is off, this proccess is skipped entirely on the DMG, but not on the CGB. + /* When the object enabled bit is off, this proccess is skipped entirely on the DMG, but not on the CGB. On the CGB, this bit is checked only when the pixel is actually popped from the FIFO. */ while (gb->n_visible_objs != 0 && @@ -1057,11 +1577,11 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) } /* Todo: Measure if penalty occurs before or after waiting for the fetcher. */ - if (gb->extra_penalty_for_sprite_at_0 != 0) { + if (gb->extra_penalty_for_object_at_0 != 0) { if (gb->obj_comparators[gb->n_visible_objs - 1] == 0) { - gb->cycles_for_line += gb->extra_penalty_for_sprite_at_0; - GB_SLEEP(gb, display, 28, gb->extra_penalty_for_sprite_at_0); - gb->extra_penalty_for_sprite_at_0 = 0; + gb->cycles_for_line += gb->extra_penalty_for_object_at_0; + GB_SLEEP(gb, display, 28, gb->extra_penalty_for_object_at_0); + gb->extra_penalty_for_object_at_0 = 0; if (gb->object_fetch_aborted) { goto abort_fetching_object; } @@ -1097,7 +1617,7 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) gb->cycles_for_line++; GB_SLEEP(gb, display, 40, 1); - const GB_object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; + const object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; uint16_t line_address = get_object_line_address(gb, object); @@ -1112,7 +1632,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--; } @@ -1127,7 +1648,17 @@ abort_fetching_object: gb->cycles_for_line++; GB_SLEEP(gb, display, 21, 1); } +skip_slow_mode_3: + /* TODO: This seems incorrect (glitches Tesserae), verify further */ + /* + 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,13 +1722,24 @@ 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++; if (gb->current_lcd_line == LINES && GB_is_sgb(gb)) { - display_vblank(gb); + GB_display_vblank(gb); } if (gb->icd_hreset_callback) { @@ -1207,35 +1749,41 @@ abort_fetching_object: gb->wx166_glitch = false; /* Lines 144 - 152 */ for (; gb->current_line < VIRTUAL_LINES - 1; gb->current_line++) { - 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 (unlikely(gb->lcd_line_callback)) { + gb->lcd_line_callback(gb, gb->current_line); } GB_STAT_update(gb); + GB_SLEEP(gb, display, 26, 2); + gb->io_registers[GB_IO_LY] = gb->current_line; + if (gb->current_line == LINES && !gb->stat_interrupt_line && (gb->io_registers[GB_IO_STAT] & 0x20)) { + gb->io_registers[GB_IO_IF] |= 2; + } 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); if (gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) { if (GB_is_cgb(gb)) { - GB_timing_sync(gb); + GB_display_vblank(gb); gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_SKIPPED; } else { if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) { gb->is_odd_frame ^= true; - display_vblank(gb); + GB_display_vblank(gb); } gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; } @@ -1243,7 +1791,7 @@ abort_fetching_object: else { if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) { gb->is_odd_frame ^= true; - display_vblank(gb); + GB_display_vblank(gb); } if (gb->frame_skip_state == GB_FRAMESKIP_FIRST_FRAME_SKIPPED) { gb->cgb_repeated_a_frame = true; @@ -1255,18 +1803,18 @@ 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. */ /* Lines 153 */ - gb->io_registers[GB_IO_LY] = 153; gb->ly_for_comparison = -1; GB_STAT_update(gb); - GB_SLEEP(gb, display, 14, (gb->model > GB_MODEL_CGB_C)? 4: 6); + GB_SLEEP(gb, display, 19, 2); + gb->io_registers[GB_IO_LY] = 153; + GB_SLEEP(gb, display, 14, (gb->model > GB_MODEL_CGB_C)? 2: 4); - if (!GB_is_cgb(gb)) { + if (gb->model <= GB_MODEL_CGB_C && !gb->cgb_double_speed) { gb->io_registers[GB_IO_LY] = 0; } gb->ly_for_comparison = 153; @@ -1285,14 +1833,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; @@ -1320,7 +1861,7 @@ void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette palette = gb->background_palettes_rgb + (4 * (palette_index & 7)); break; case GB_PALETTE_OAM: - palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7)); + palette = gb->object_palettes_rgb + (4 * (palette_index & 7)); break; } @@ -1370,7 +1911,7 @@ void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette palette = gb->background_palettes_rgb + (4 * (palette_index & 7)); break; case GB_PALETTE_OAM: - palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7)); + palette = gb->object_palettes_rgb + (4 * (palette_index & 7)); break; case GB_PALETTE_AUTO: break; @@ -1422,31 +1963,31 @@ void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette } } -uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height) +uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *object_height) { uint8_t count = 0; - *sprite_height = (gb->io_registers[GB_IO_LCDC] & 4) ? 16:8; + *object_height = (gb->io_registers[GB_IO_LCDC] & 4) ? 16:8; uint8_t oam_to_dest_index[40] = {0,}; - for (unsigned y = 0; y < LINES; y++) { - GB_object_t *sprite = (GB_object_t *) &gb->oam; - uint8_t sprites_in_line = 0; - for (uint8_t i = 0; i < 40; i++, sprite++) { - signed sprite_y = sprite->y - 16; + for (signed y = 0; y < LINES; y++) { + object_t *object = (object_t *) &gb->oam; + uint8_t objects_in_line = 0; + for (uint8_t i = 0; i < 40; i++, object++) { + signed object_y = object->y - 16; bool obscured = false; - // Is sprite not in this line? - if (sprite_y > y || sprite_y + *sprite_height <= y) continue; - if (++sprites_in_line == 11) obscured = true; + // Is object not in this line? + if (object_y > y || object_y + *object_height <= y) continue; + if (++objects_in_line == 11) obscured = true; GB_oam_info_t *info = NULL; if (!oam_to_dest_index[i]) { info = dest + count; oam_to_dest_index[i] = ++count; - info->x = sprite->x; - info->y = sprite->y; - info->tile = *sprite_height == 16? sprite->tile & 0xFE : sprite->tile; - info->flags = sprite->flags; + info->x = object->x; + info->y = object->y; + info->tile = *object_height == 16? object->tile & 0xFE : object->tile; + info->flags = object->flags; info->obscured_by_line_limit = false; - info->oam_addr = 0xFE00 + i * sizeof(*sprite); + info->oam_addr = 0xFE00 + i * sizeof(*object); } else { info = dest + oam_to_dest_index[i] - 1; @@ -1463,16 +2004,15 @@ uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_h vram_address += 0x2000; } - for (unsigned y = 0; y < *sprite_height; y++) { - UNROLL - for (unsigned x = 0; x < 8; x++) { + for (unsigned y = 0; y < *object_height; y++) { + 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 ); if (!gb->cgb_mode) { color = (gb->io_registers[palette? GB_IO_OBP1:GB_IO_OBP0] >> (color << 1)) & 3; } - dest[i].image[((flags & 0x20)?7-x:x) + ((flags & 0x40)?*sprite_height - 1 -y:y) * 8] = gb->sprite_palettes_rgb[palette * 4 + color]; + dest[i].image[((flags & 0x20)?7-x:x) + ((flags & 0x40)?*object_height - 1 -y:y) * 8] = gb->object_palettes_rgb[palette * 4 + color]; } vram_address += 2; } @@ -1485,3 +2025,24 @@ bool GB_is_odd_frame(GB_gameboy_t *gb) { return gb->is_odd_frame; } + +void GB_set_object_rendering_disabled(GB_gameboy_t *gb, bool disabled) +{ + gb->objects_disabled = disabled; +} + +void GB_set_background_rendering_disabled(GB_gameboy_t *gb, bool disabled) +{ + gb->background_disabled = disabled; +} + +bool GB_is_object_rendering_disabled(GB_gameboy_t *gb) +{ + return gb->objects_disabled; +} + +bool GB_is_background_rendering_disabled(GB_gameboy_t *gb) +{ + return gb->background_disabled; +} + diff --git a/Core/display.h b/Core/display.h index 5bdeba8..d50dc18 100644 --- a/Core/display.h +++ b/Core/display.h @@ -6,13 +6,14 @@ #include #ifdef GB_INTERNAL -void GB_display_run(GB_gameboy_t *gb, uint8_t cycles); -void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index); -void GB_STAT_update(GB_gameboy_t *gb); -void GB_lcd_off(GB_gameboy_t *gb); +internal void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force); +internal void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index); +internal void GB_STAT_update(GB_gameboy_t *gb); +internal void GB_lcd_off(GB_gameboy_t *gb); +internal void GB_display_vblank(GB_gameboy_t *gb); +#define GB_display_sync(gb) GB_display_run(gb, 0, true) enum { - GB_OBJECT_PRIORITY_UNDEFINED, // For save state compatibility GB_OBJECT_PRIORITY_X, GB_OBJECT_PRIORITY_INDEX, }; @@ -51,12 +52,21 @@ 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); void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index, GB_map_type_t map_type, GB_tileset_type_t tileset_type); -uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height); +uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *object_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); + +void GB_set_object_rendering_disabled(GB_gameboy_t *gb, bool disabled); +void GB_set_background_rendering_disabled(GB_gameboy_t *gb, bool disabled); +bool GB_is_object_rendering_disabled(GB_gameboy_t *gb); +bool GB_is_background_rendering_disabled(GB_gameboy_t *gb); + + #endif /* display_h */ diff --git a/Core/gb.c b/Core/gb.c index 7325d79..632c5a8 100644 --- a/Core/gb.c +++ b/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,25 +109,33 @@ 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(); } + else if (gb->model == GB_MODEL_MGB) { + #include "graphics/mgb_border.inc" + LOAD_BORDER(); + if (gb->dmg_palette && + gb->dmg_palette->colors[4].b > gb->dmg_palette->colors[4].r) { + for (unsigned i = 0; i < 7; i++) { + gb->borrowed_border.map[13 + 24 * 32 + i] = i + 1; + gb->borrowed_border.map[13 + 25 * 32 + i] = i + 8; + } + } + } else if (GB_is_cgb(gb)) { #include "graphics/cgb_border.inc" LOAD_BORDER(); @@ -202,6 +204,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 @@ -290,7 +295,7 @@ int GB_load_rom(GB_gameboy_t *gb, const char *path) gb->rom_size |= gb->rom_size >> 1; gb->rom_size++; } - if (gb->rom_size == 0) { + if (gb->rom_size < 0x8000) { gb->rom_size = 0x8000; } fseek(f, 0, SEEK_SET); @@ -302,9 +307,184 @@ 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 < 0x8000) { + 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; + gb->mbc_ram_size = 0; + } + + 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"); @@ -316,11 +496,8 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) #define READ(x) if (fread(&x, sizeof(x), 1, f) != 1) goto error fread(magic, 1, sizeof(magic), f); -#ifdef GB_BIG_ENDIAN - bool extended = *(uint32_t *)&magic == 'ISX '; -#else - bool extended = *(uint32_t *)&magic == __builtin_bswap32('ISX '); -#endif + + bool extended = *(uint32_t *)&magic == BE32('ISX '); fseek(f, extended? 0x20 : 0, SEEK_SET); @@ -347,15 +524,11 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) } READ(address); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap16(address); -#endif + address = LE16(address); address &= 0x3FFF; READ(length); -#ifdef GB_BIG_ENDIAN - length = __builtin_bswap16(length); -#endif + length = LE16(length); size_t needed_size = bank * 0x4000 + address + length; if (needed_size > 1024 * 1024 * 32) goto error; @@ -376,14 +549,10 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) uint32_t length; READ(address); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap32(address); -#endif + address = LE32(address); READ(length); -#ifdef GB_BIG_ENDIAN - length = __builtin_bswap32(length); -#endif + length = LE32(length); size_t needed_size = address + length; if (needed_size > 1024 * 1024 * 32) goto error; @@ -407,9 +576,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) uint16_t address; uint8_t byte; READ(count); -#ifdef GB_BIG_ENDIAN - count = __builtin_bswap16(count); -#endif + count = LE16(count); while (count--) { READ(length); if (fread(name, length, 1, f) != 1) goto error; @@ -424,9 +591,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) } READ(address); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap16(address); -#endif + address = LE16(address); GB_debugger_add_symbol(gb, bank, address, name); } break; @@ -439,9 +604,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) uint8_t flag; uint32_t address; READ(count); -#ifdef GB_BIG_ENDIAN - count = __builtin_bswap16(count); -#endif + count = LE16(count); while (count--) { READ(length); if (fread(name, length + 1, 1, f) != 1) goto error; @@ -449,9 +612,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) READ(flag); // unused READ(address); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap32(address); -#endif + address = LE32(address); // TODO: How to convert 32-bit addresses to Bank:Address? Needs to tell RAM and ROM apart } break; @@ -534,6 +695,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 +718,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 { @@ -567,15 +734,16 @@ typedef struct { uint8_t padding4[3]; uint8_t high; uint8_t padding5[3]; -} GB_vba_rtc_time_t; +} 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]; +} tpp1_rtc_save_t; typedef union { struct __attribute__((packed)) { @@ -584,63 +752,78 @@ typedef union { } sameboy_legacy; struct { /* Used by VBA versions with 32-bit timestamp*/ - GB_vba_rtc_time_t rtc_real, rtc_latched; + vba_rtc_time_t rtc_real, rtc_latched; uint32_t last_rtc_second; /* Always little endian */ } vba32; struct { /* Used by BGB and VBA versions with 64-bit timestamp*/ - GB_vba_rtc_time_t rtc_real, rtc_latched; + vba_rtc_time_t rtc_real, rtc_latched; uint64_t last_rtc_second; /* Always little endian */ } vba64; -} GB_rtc_save_t; +} rtc_save_t; + +static void fill_tpp1_save_data(GB_gameboy_t *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); } - GB_rtc_save_t rtc_save_size; + + if (gb->cartridge_type->mbc_type == GB_TPP1) { + return gb->mbc_ram_size + sizeof(tpp1_rtc_save_t); + } + + rtc_save_t rtc_save_size; return gb->mbc_ram_size + (gb->cartridge_type->has_rtc ? sizeof(rtc_save_size.vba64) : 0); } 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; + tpp1_rtc_save_t rtc_save; + 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 GB_huc3_rtc_time_t rtc_save = { - __builtin_bswap64(gb->last_rtc_second), - __builtin_bswap16(gb->huc3_minutes), - __builtin_bswap16(gb->huc3_days), - __builtin_bswap16(gb->huc3_alarm_minutes), - __builtin_bswap16(gb->huc3_alarm_days), - gb->huc3_alarm_enabled, + 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, }; -#else - GB_huc3_rtc_time_t rtc_save = { - gb->last_rtc_second, - gb->huc3_minutes, - gb->huc3_days, - gb->huc3_alarm_minutes, - gb->huc3_alarm_days, - gb->huc3_alarm_enabled, - }; -#endif memcpy(buffer, &rtc_save, sizeof(rtc_save)); } else if (gb->cartridge_type->has_rtc) { - GB_rtc_save_t rtc_save = {{{{0,}},},}; + rtc_save_t rtc_save = {{{{0,}},},}; rtc_save.vba64.rtc_real.seconds = gb->rtc_real.seconds; rtc_save.vba64.rtc_real.minutes = gb->rtc_real.minutes; rtc_save.vba64.rtc_real.hours = gb->rtc_real.hours; @@ -651,11 +834,7 @@ int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) rtc_save.vba64.rtc_latched.hours = gb->rtc_latched.hours; 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); -#else - rtc_save.vba64.last_rtc_second = gb->last_rtc_second; -#endif + rtc_save.vba64.last_rtc_second = LE64(time(NULL)); memcpy(buffer + gb->mbc_ram_size, &rtc_save.vba64, sizeof(rtc_save.vba64)); } @@ -666,6 +845,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,26 +857,24 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) fclose(f); return EIO; } - if (gb->cartridge_type->mbc_type == GB_HUC3) { -#ifdef GB_BIG_ENDIAN + if (gb->cartridge_type->mbc_type == GB_TPP1) { + tpp1_rtc_save_t rtc_save; + 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) { GB_huc3_rtc_time_t rtc_save = { - __builtin_bswap64(gb->last_rtc_second), - __builtin_bswap16(gb->huc3_minutes), - __builtin_bswap16(gb->huc3_days), - __builtin_bswap16(gb->huc3_alarm_minutes), - __builtin_bswap16(gb->huc3_alarm_days), - gb->huc3_alarm_enabled, + 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, }; -#else - GB_huc3_rtc_time_t rtc_save = { - gb->last_rtc_second, - gb->huc3_minutes, - gb->huc3_days, - gb->huc3_alarm_minutes, - gb->huc3_alarm_days, - gb->huc3_alarm_enabled, - }; -#endif if (fwrite(&rtc_save, sizeof(rtc_save), 1, f) != 1) { fclose(f); @@ -704,7 +882,7 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) } } else if (gb->cartridge_type->has_rtc) { - GB_rtc_save_t rtc_save = {{{{0,}},},}; + rtc_save_t rtc_save = {{{{0,}},},}; rtc_save.vba64.rtc_real.seconds = gb->rtc_real.seconds; rtc_save.vba64.rtc_real.minutes = gb->rtc_real.minutes; rtc_save.vba64.rtc_real.hours = gb->rtc_real.hours; @@ -715,11 +893,7 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) rtc_save.vba64.rtc_latched.hours = gb->rtc_latched.hours; 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); -#else - rtc_save.vba64.last_rtc_second = gb->last_rtc_second; -#endif + rtc_save.vba64.last_rtc_second = LE64(time(NULL)); if (fwrite(&rtc_save.vba64, 1, sizeof(rtc_save.vba64), f) != sizeof(rtc_save.vba64)) { fclose(f); return EIO; @@ -732,6 +906,14 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) return errno; } +static void load_tpp1_save_data(GB_gameboy_t *gb, const 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,27 +921,34 @@ 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) { + 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)); + + 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)) { goto reset_rtc; } memcpy(&rtc_save, buffer + gb->mbc_ram_size, sizeof(rtc_save)); -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); - gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); - gb->huc3_days = __builtin_bswap16(rtc_save.days); - gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); - gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#else - gb->last_rtc_second = rtc_save.last_rtc_second; - gb->huc3_minutes = rtc_save.minutes; - gb->huc3_days = rtc_save.days; - gb->huc3_alarm_minutes = rtc_save.alarm_minutes; - gb->huc3_alarm_days = rtc_save.alarm_days; - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#endif + gb->last_rtc_second = LE64(rtc_save.last_rtc_second); + gb->huc3.minutes = LE16(rtc_save.minutes); + gb->huc3.days = LE16(rtc_save.days); + gb->huc3.alarm_minutes = LE16(rtc_save.alarm_minutes); + gb->huc3.alarm_days = LE16(rtc_save.alarm_days); + gb->huc3.alarm_enabled = rtc_save.alarm_enabled; if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ goto reset_rtc; @@ -767,7 +956,7 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t return; } - GB_rtc_save_t rtc_save; + rtc_save_t rtc_save; memcpy(&rtc_save, buffer + gb->mbc_ram_size, MIN(sizeof(rtc_save), size)); switch (size - gb->mbc_ram_size) { case sizeof(rtc_save.sameboy_legacy): @@ -787,11 +976,7 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t gb->rtc_latched.hours = rtc_save.vba32.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba32.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba32.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap32(rtc_save.vba32.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba32.last_rtc_second; -#endif + gb->last_rtc_second = LE32(rtc_save.vba32.last_rtc_second); break; case sizeof(rtc_save.vba64): @@ -805,11 +990,7 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t gb->rtc_latched.hours = rtc_save.vba64.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba64.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba64.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.vba64.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba64.last_rtc_second; -#endif + gb->last_rtc_second = LE64(rtc_save.vba64.last_rtc_second); break; default: @@ -829,9 +1010,11 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t reset_rtc: gb->last_rtc_second = time(NULL); gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ - gb->huc3_days = 0xFFFF; - gb->huc3_minutes = 0xFFF; - gb->huc3_alarm_enabled = false; + if (gb->cartridge_type->mbc_type == GB_HUC3) { + gb->huc3.days = 0xFFFF; + gb->huc3.minutes = 0xFFF; + gb->huc3.alarm_enabled = false; + } exit: return; } @@ -848,26 +1031,33 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) goto reset_rtc; } + if (gb->cartridge_type->mbc_type == GB_TPP1) { + tpp1_rtc_save_t rtc_save; + if (fread(&rtc_save, sizeof(rtc_save), 1, f) != 1) { + goto reset_rtc; + } + + 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) { goto reset_rtc; } -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); - gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); - gb->huc3_days = __builtin_bswap16(rtc_save.days); - gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); - gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#else - gb->last_rtc_second = rtc_save.last_rtc_second; - gb->huc3_minutes = rtc_save.minutes; - gb->huc3_days = rtc_save.days; - gb->huc3_alarm_minutes = rtc_save.alarm_minutes; - gb->huc3_alarm_days = rtc_save.alarm_days; - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#endif + gb->last_rtc_second = LE64(rtc_save.last_rtc_second); + gb->huc3.minutes = LE16(rtc_save.minutes); + gb->huc3.days = LE16(rtc_save.days); + gb->huc3.alarm_minutes = LE16(rtc_save.alarm_minutes); + gb->huc3.alarm_days = LE16(rtc_save.alarm_days); + gb->huc3.alarm_enabled = rtc_save.alarm_enabled; + if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ goto reset_rtc; @@ -875,7 +1065,7 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) return; } - GB_rtc_save_t rtc_save; + rtc_save_t rtc_save; switch (fread(&rtc_save, 1, sizeof(rtc_save), f)) { case sizeof(rtc_save.sameboy_legacy): memcpy(&gb->rtc_real, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); @@ -894,11 +1084,7 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) gb->rtc_latched.hours = rtc_save.vba32.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba32.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba32.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap32(rtc_save.vba32.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba32.last_rtc_second; -#endif + gb->last_rtc_second = LE32(rtc_save.vba32.last_rtc_second); break; case sizeof(rtc_save.vba64): @@ -912,11 +1098,7 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) gb->rtc_latched.hours = rtc_save.vba64.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba64.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba64.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.vba64.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba64.last_rtc_second; -#endif + gb->last_rtc_second = LE64(rtc_save.vba64.last_rtc_second); break; default: @@ -936,9 +1118,11 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) reset_rtc: gb->last_rtc_second = time(NULL); gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ - gb->huc3_days = 0xFFFF; - gb->huc3_minutes = 0xFFF; - gb->huc3_alarm_enabled = false; + if (gb->cartridge_type->mbc_type == GB_HUC3) { + gb->huc3.days = 0xFFFF; + gb->huc3.minutes = 0xFFF; + gb->huc3.alarm_enabled = false; + } exit: fclose(f); return; @@ -948,14 +1132,14 @@ uint8_t GB_run(GB_gameboy_t *gb) { gb->vblank_just_occured = false; - if (gb->sgb && gb->sgb->intro_animation < 140) { + if (gb->sgb && gb->sgb->intro_animation < 96) { /* On the SGB, the GB is halted after finishing the boot ROM. Then, after the boot animation is almost done, it's reset. Since the SGB HLE does not perform any header validity checks, we just halt the CPU (with hacky code) until the correct time. This ensures the Nintendo logo doesn't flash on screen, and the game does "run in background" while the animation is playing. */ - GB_display_run(gb, 228); + GB_display_run(gb, 228, true); gb->cycles_since_last_sync += 228; return 228; } @@ -964,10 +1148,12 @@ 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); } + if (!(gb->io_registers[GB_IO_IF] & 0x10) && (gb->io_registers[GB_IO_JOYP] & 0x30) != 0x30) { + gb->joyp_accessed = true; + } return gb->cycles_since_run; } @@ -1023,6 +1209,16 @@ void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) #endif } +void GB_set_execution_callback(GB_gameboy_t *gb, GB_execution_callback_t callback) +{ + gb->execution_callback = callback; +} + +void GB_set_lcd_line_callback(GB_gameboy_t *gb, GB_lcd_line_callback_t callback) +{ + gb->lcd_line_callback = callback; +} + const GB_palette_t GB_PALETTE_GREY = {{{0x00, 0x00, 0x00}, {0x55, 0x55, 0x55}, {0xaa, 0xaa, 0xaa}, {0xff, 0xff, 0xff}, {0xff, 0xff, 0xff}}}; const GB_palette_t GB_PALETTE_DMG = {{{0x08, 0x18, 0x10}, {0x39, 0x61, 0x39}, {0x84, 0xa5, 0x63}, {0xc6, 0xde, 0x8c}, {0xd2, 0xe6, 0xa6}}}; const GB_palette_t GB_PALETTE_MGB = {{{0x07, 0x10, 0x0e}, {0x3a, 0x4c, 0x3a}, {0x81, 0x8d, 0x66}, {0xc2, 0xce, 0x93}, {0xcf, 0xda, 0xac}}}; @@ -1032,13 +1228,13 @@ static void update_dmg_palette(GB_gameboy_t *gb) { const GB_palette_t *palette = gb->dmg_palette ?: &GB_PALETTE_GREY; if (gb->rgb_encode_callback && !GB_is_cgb(gb)) { - gb->sprite_palettes_rgb[4] = gb->sprite_palettes_rgb[0] = gb->background_palettes_rgb[0] = + gb->object_palettes_rgb[4] = gb->object_palettes_rgb[0] = gb->background_palettes_rgb[0] = gb->rgb_encode_callback(gb, palette->colors[3].r, palette->colors[3].g, palette->colors[3].b); - gb->sprite_palettes_rgb[5] = gb->sprite_palettes_rgb[1] = gb->background_palettes_rgb[1] = + gb->object_palettes_rgb[5] = gb->object_palettes_rgb[1] = gb->background_palettes_rgb[1] = gb->rgb_encode_callback(gb, palette->colors[2].r, palette->colors[2].g, palette->colors[2].b); - gb->sprite_palettes_rgb[6] = gb->sprite_palettes_rgb[2] = gb->background_palettes_rgb[2] = + gb->object_palettes_rgb[6] = gb->object_palettes_rgb[2] = gb->background_palettes_rgb[2] = gb->rgb_encode_callback(gb, palette->colors[1].r, palette->colors[1].g, palette->colors[1].b); - gb->sprite_palettes_rgb[7] = gb->sprite_palettes_rgb[3] = gb->background_palettes_rgb[3] = + gb->object_palettes_rgb[7] = gb->object_palettes_rgb[3] = gb->background_palettes_rgb[3] = gb->rgb_encode_callback(gb, palette->colors[0].r, palette->colors[0].g, palette->colors[0].b); // LCD off color @@ -1053,6 +1249,11 @@ void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette) update_dmg_palette(gb); } +const GB_palette_t *GB_get_palette(GB_gameboy_t *gb) +{ + return gb->dmg_palette; +} + void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback) { @@ -1073,17 +1274,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 +1300,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) { @@ -1141,9 +1332,14 @@ bool GB_is_inited(GB_gameboy_t *gb) return gb->magic == state_magic(); } -bool GB_is_cgb(GB_gameboy_t *gb) +bool GB_is_cgb(const GB_gameboy_t *gb) { - return (gb->model & GB_MODEL_FAMILY_MASK) == GB_MODEL_CGB_FAMILY; + return gb->model >= GB_MODEL_CGB_0; +} + +bool GB_is_cgb_in_cgb_mode(GB_gameboy_t *gb) +{ + return gb->cgb_mode; } bool GB_is_sgb(GB_gameboy_t *gb) @@ -1180,6 +1376,7 @@ void GB_set_user_data(GB_gameboy_t *gb, void *data) static void reset_ram(GB_gameboy_t *gb) { switch (gb->model) { + case GB_MODEL_MGB: case GB_MODEL_CGB_E: case GB_MODEL_AGB: /* Unverified */ for (unsigned i = 0; i < gb->ram_size; i++) { @@ -1210,14 +1407,28 @@ static void reset_ram(GB_gameboy_t *gb) gb->ram[i] ^= GB_random() & GB_random() & GB_random(); } break; - + + case GB_MODEL_CGB_0: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: for (unsigned i = 0; i < gb->ram_size; i++) { if ((i & 0x808) == 0x800 || (i & 0x808) == 0x008) { gb->ram[i] = 0; } else { - gb->ram[i] = GB_random() | GB_random() | GB_random() | GB_random(); + gb->ram[i] = GB_random() | GB_random() | GB_random() | GB_random() | GB_random(); + } + } + break; + case GB_MODEL_CGB_D: + for (unsigned i = 0; i < gb->ram_size; i++) { + gb->ram[i] = GB_random(); + if (i & 0x800) { + gb->ram[i] &= GB_random(); + } + else { + gb->ram[i] |= GB_random(); } } break; @@ -1225,8 +1436,11 @@ static void reset_ram(GB_gameboy_t *gb) /* HRAM */ switch (gb->model) { + case GB_MODEL_CGB_0: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: - // case GB_MODEL_CGB_D: + case GB_MODEL_CGB_D: case GB_MODEL_CGB_E: case GB_MODEL_AGB: for (unsigned i = 0; i < sizeof(gb->hram); i++) { @@ -1235,6 +1449,7 @@ static void reset_ram(GB_gameboy_t *gb) break; case GB_MODEL_DMG_B: + case GB_MODEL_MGB: case GB_MODEL_SGB_NTSC: /* Unverified*/ case GB_MODEL_SGB_PAL: /* Unverified */ case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ @@ -1254,13 +1469,18 @@ static void reset_ram(GB_gameboy_t *gb) /* OAM */ switch (gb->model) { + case GB_MODEL_CGB_0: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: + case GB_MODEL_CGB_D: case GB_MODEL_CGB_E: case GB_MODEL_AGB: - /* Zero'd out by boot ROM anyway*/ + /* Zero'd out by boot ROM anyway, extra OAM no accessible */ break; case GB_MODEL_DMG_B: + case GB_MODEL_MGB: case GB_MODEL_SGB_NTSC: /* Unverified */ case GB_MODEL_SGB_PAL: /* Unverified */ case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ @@ -1283,12 +1503,26 @@ static void reset_ram(GB_gameboy_t *gb) /* Wave RAM */ switch (gb->model) { + case GB_MODEL_CGB_0: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: + case GB_MODEL_CGB_D: case GB_MODEL_CGB_E: case GB_MODEL_AGB: - /* Initialized by CGB-A and newer, 0s in CGB-0*/ + /* Initialized by CGB-A and newer, 0s in CGB-0 */ break; - + case GB_MODEL_MGB: { + for (unsigned i = 0; i < GB_IO_WAV_END - GB_IO_WAV_START; i++) { + if (i & 1) { + gb->io_registers[GB_IO_WAV_START + i] = GB_random() & GB_random(); + } + else { + gb->io_registers[GB_IO_WAV_START + i] = GB_random() | GB_random(); + } + } + break; + } case GB_MODEL_DMG_B: case GB_MODEL_SGB_NTSC: /* Unverified*/ case GB_MODEL_SGB_PAL: /* Unverified */ @@ -1296,18 +1530,13 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ case GB_MODEL_SGB2: case GB_MODEL_SGB2_NO_SFC: { - uint8_t temp; for (unsigned i = 0; i < GB_IO_WAV_END - GB_IO_WAV_START; i++) { if (i & 1) { - temp = GB_random() & GB_random() & GB_random(); + gb->io_registers[GB_IO_WAV_START + i] = GB_random() & GB_random() & GB_random(); } else { - temp = GB_random() | GB_random() | GB_random(); + gb->io_registers[GB_IO_WAV_START + i] = GB_random() | GB_random() | GB_random(); } - gb->apu.wave_channel.wave_form[i * 2] = temp >> 4; - gb->apu.wave_channel.wave_form[i * 2 + 1] = temp & 0xF; - gb->io_registers[GB_IO_WAV_START + i] = temp; - } break; } @@ -1320,7 +1549,7 @@ static void reset_ram(GB_gameboy_t *gb) if (GB_is_cgb(gb)) { for (unsigned i = 0; i < 64; i++) { gb->background_palettes_data[i] = GB_random(); /* Doesn't really matter as the boot ROM overrides it anyway*/ - gb->sprite_palettes_data[i] = GB_random(); + gb->object_palettes_data[i] = GB_random(); } for (unsigned i = 0; i < 32; i++) { GB_palette_changed(gb, true, i * 2); @@ -1337,6 +1566,9 @@ static void request_boot_rom(GB_gameboy_t *gb) case GB_MODEL_DMG_B: type = GB_BOOT_ROM_DMG; break; + case GB_MODEL_MGB: + type = GB_BOOT_ROM_MGB; + break; case GB_MODEL_SGB_NTSC: case GB_MODEL_SGB_PAL: case GB_MODEL_SGB_NTSC_NO_SFC: @@ -1347,7 +1579,13 @@ static void request_boot_rom(GB_gameboy_t *gb) case GB_MODEL_SGB2_NO_SFC: type = GB_BOOT_ROM_SGB2; break; + case GB_MODEL_CGB_0: + type = GB_BOOT_ROM_CGB_0; + break; + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: + case GB_MODEL_CGB_D: case GB_MODEL_CGB_E: type = GB_BOOT_ROM_CGB; break; @@ -1363,7 +1601,11 @@ void GB_reset(GB_gameboy_t *gb) { uint32_t mbc_ram_size = gb->mbc_ram_size; GB_model_t model = gb->model; + GB_update_clock_rate(gb); + uint8_t rtc_section[GB_SECTION_SIZE(rtc)]; + memcpy(rtc_section, GB_GET_SECTION(gb, rtc), sizeof(rtc_section)); memset(gb, 0, (size_t)GB_GET_SECTION((GB_gameboy_t *) 0, unsaved)); + memcpy(GB_GET_SECTION(gb, rtc), rtc_section, sizeof(rtc_section)); gb->model = model; gb->version = GB_STRUCT_VERSION; @@ -1420,11 +1662,8 @@ void GB_reset(GB_gameboy_t *gb) } } - /* Todo: Ugly, fixme, see comment in the timer state machine */ - gb->div_state = 3; + GB_set_internal_div_counter(gb, 8); - GB_apu_update_cycles_per_sample(gb); - if (gb->nontrivial_jump_state) { free(gb->nontrivial_jump_state); gb->nontrivial_jump_state = NULL; @@ -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); @@ -1502,9 +1745,9 @@ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t * *bank = 0; return &gb->background_palettes_data; case GB_DIRECT_ACCESS_OBP: - *size = sizeof(gb->sprite_palettes_data); + *size = sizeof(gb->object_palettes_data); *bank = 0; - return &gb->sprite_palettes_data; + return &gb->object_palettes_data; case GB_DIRECT_ACCESS_IE: *size = sizeof(gb->interrupt_enable); *bank = 0; @@ -1516,21 +1759,40 @@ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t * } } +GB_registers_t *GB_get_registers(GB_gameboy_t *gb) +{ + return (GB_registers_t *)&gb->registers; +} + void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier) { gb->clock_multiplier = multiplier; - GB_apu_update_cycles_per_sample(gb); + GB_update_clock_rate(gb); } uint32_t GB_get_clock_rate(GB_gameboy_t *gb) +{ + return gb->clock_rate; +} + +uint32_t GB_get_unmultiplied_clock_rate(GB_gameboy_t *gb) +{ + return gb->unmultiplied_clock_rate; +} + +void GB_update_clock_rate(GB_gameboy_t *gb) { if (gb->model & GB_MODEL_PAL_BIT) { - return SGB_PAL_FREQUENCY * gb->clock_multiplier; + gb->unmultiplied_clock_rate = SGB_PAL_FREQUENCY; } - if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) { - return SGB_NTSC_FREQUENCY * gb->clock_multiplier; + else if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) { + gb->unmultiplied_clock_rate = SGB_NTSC_FREQUENCY; } - return CPU_FREQUENCY * gb->clock_multiplier; + else { + gb->unmultiplied_clock_rate = CPU_FREQUENCY; + } + + gb->clock_rate = gb->unmultiplied_clock_rate * gb->clock_multiplier; } void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode) @@ -1610,10 +1872,89 @@ void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t unsigned GB_time_to_alarm(GB_gameboy_t *gb) { if (gb->cartridge_type->mbc_type != GB_HUC3) return 0; - if (!gb->huc3_alarm_enabled) return 0; - if (!(gb->huc3_alarm_days & 0x2000)) return 0; - unsigned current_time = (gb->huc3_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_minutes * 60 + (time(NULL) % 60); - unsigned alarm_time = (gb->huc3_alarm_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_alarm_minutes * 60; + if (!gb->huc3.alarm_enabled) return 0; + if (!(gb->huc3.alarm_days & 0x2000)) return 0; + unsigned current_time = (gb->huc3.days & 0x1FFF) * 24 * 60 * 60 + gb->huc3.minutes * 60 + (time(NULL) % 60); + unsigned alarm_time = (gb->huc3.alarm_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3.alarm_minutes * 60; if (current_time > alarm_time) return 0; return alarm_time - current_time; } + +bool GB_has_accelerometer(GB_gameboy_t *gb) +{ + return gb->cartridge_type->mbc_type == GB_MBC7; +} + +void GB_set_accelerometer_values(GB_gameboy_t *gb, double x, double y) +{ + gb->accelerometer_x = x; + gb->accelerometer_y = y; +} + +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/Core/gb.h b/Core/gb.h index 9043936..c116cbc 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -3,9 +3,10 @@ #define typeof __typeof__ #include #include +#include #include -#include "gb_struct_def.h" +#include "defs.h" #include "save_state.h" #include "apu.h" @@ -24,26 +25,16 @@ #include "cheats.h" #include "rumble.h" #include "workboy.h" +#include "random.h" -#define GB_STRUCT_VERSION 13 +#define GB_STRUCT_VERSION 14 #define GB_MODEL_FAMILY_MASK 0xF00 #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 - -#ifdef GB_INTERNAL -#if __clang__ -#define UNROLL _Pragma("unroll") -#elif __GNUC__ >= 8 -#define UNROLL _Pragma("GCC unroll 8") -#else -#define UNROLL -#endif - -#endif +#define GB_MODEL_PAL_BIT 0x40 +#define GB_MODEL_NO_SFC_BIT 0x80 #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ #define GB_BIG_ENDIAN @@ -53,12 +44,9 @@ #error Unable to detect endianess #endif -#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8) -#define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; }) -#endif typedef struct { - struct { + struct GB_color_s { uint8_t r, g, b; } colors[5]; } GB_palette_t; @@ -76,9 +64,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, @@ -90,14 +93,14 @@ typedef enum { GB_MODEL_SGB_NTSC_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT, GB_MODEL_SGB_NO_SFC = GB_MODEL_SGB_NTSC_NO_SFC, GB_MODEL_SGB_PAL_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT | GB_MODEL_PAL_BIT, - // GB_MODEL_MGB = 0x100, + GB_MODEL_MGB = 0x100, GB_MODEL_SGB2 = 0x101, GB_MODEL_SGB2_NO_SFC = GB_MODEL_SGB2 | GB_MODEL_NO_SFC_BIT, - // GB_MODEL_CGB_0 = 0x200, - // GB_MODEL_CGB_A = 0x201, - // GB_MODEL_CGB_B = 0x202, + GB_MODEL_CGB_0 = 0x200, + GB_MODEL_CGB_A = 0x201, + GB_MODEL_CGB_B = 0x202, GB_MODEL_CGB_C = 0x203, - // GB_MODEL_CGB_D = 0x204, + GB_MODEL_CGB_D = 0x204, GB_MODEL_CGB_E = 0x205, GB_MODEL_AGB = 0x206, } GB_model_t; @@ -108,6 +111,7 @@ enum { GB_REGISTER_DE, GB_REGISTER_HL, GB_REGISTER_SP, + GB_REGISTER_PC, GB_REGISTERS_16_BIT /* Count */ }; @@ -125,8 +129,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) @@ -188,10 +190,7 @@ enum { GB_IO_OBP1 = 0x49, // Object Palette 1 Data (R/W) - Non CGB Mode Only GB_IO_WY = 0x4a, // Window Y Position (R/W) GB_IO_WX = 0x4b, // Window X Position minus 7 (R/W) - // Has some undocumented compatibility flags written at boot. - // Unfortunately it is not readable or writable after boot has finished, so research of this - // register is quite limited. The value written to this register, however, can be controlled - // in some cases. + // Controls DMG mode and PGB mode GB_IO_KEY0 = 0x4c, /* General CGB features */ @@ -217,20 +216,19 @@ enum { /* CGB Paletts */ GB_IO_BGPI = 0x68, // CGB Mode Only - Background Palette Index GB_IO_BGPD = 0x69, // CGB Mode Only - Background Palette Data - GB_IO_OBPI = 0x6a, // CGB Mode Only - Sprite Palette Index - GB_IO_OBPD = 0x6b, // CGB Mode Only - Sprite Palette Data + GB_IO_OBPI = 0x6a, // CGB Mode Only - Object Palette Index + GB_IO_OBPD = 0x6b, // CGB Mode Only - Object Palette Data GB_IO_OPRI = 0x6c, // Affects object priority (X based or index based) /* Missing */ GB_IO_SVBK = 0x70, // CGB Mode Only - WRAM Bank - GB_IO_UNKNOWN2 = 0x72, // (00h) - Bit 0-7 (Read/Write) - GB_IO_UNKNOWN3 = 0x73, // (00h) - Bit 0-7 (Read/Write) - GB_IO_UNKNOWN4 = 0x74, // (00h) - Bit 0-7 (Read/Write) - CGB Mode Only + GB_IO_PSWX = 0x72, // X position of the palette switching window + GB_IO_PSWY = 0x73, // Y position of the palette switching window + GB_IO_PSW = 0x74, // Key combo to trigger the palette switching window GB_IO_UNKNOWN5 = 0x75, // (8Fh) - Bit 4-6 (Read/Write) - GB_IO_PCM_12 = 0x76, // Channels 1 and 2 amplitudes - GB_IO_PCM_34 = 0x77, // Channels 3 and 4 amplitudes - GB_IO_UNKNOWN8 = 0x7F, // Unknown, write only + GB_IO_PCM12 = 0x76, // Channels 1 and 2 amplitudes + GB_IO_PCM34 = 0x77, // Channels 3 and 4 amplitudes }; typedef enum { @@ -241,12 +239,12 @@ typedef enum { } GB_log_attributes; typedef enum { - GB_BOOT_ROM_DMG0, + GB_BOOT_ROM_DMG_0, GB_BOOT_ROM_DMG, GB_BOOT_ROM_MGB, GB_BOOT_ROM_SGB, GB_BOOT_ROM_SGB2, - GB_BOOT_ROM_CGB0, + GB_BOOT_ROM_CGB_0, GB_BOOT_ROM_CGB, GB_BOOT_ROM_AGB, } GB_boot_rom_t; @@ -257,7 +255,6 @@ typedef enum { #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 +269,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,10 +280,8 @@ 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; +typedef void (*GB_execution_callback_t)(GB_gameboy_t *gb, uint16_t address, uint8_t opcode); +typedef void (*GB_lcd_line_callback_t)(GB_gameboy_t *gb, uint8_t line); struct GB_breakpoint_s; struct GB_watchpoint_s; @@ -294,8 +289,8 @@ struct GB_watchpoint_s; typedef struct { uint8_t pixel; // Color, 0-3 uint8_t palette; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) - uint8_t priority; // Sprite priority – 0 in DMG, OAM index in CGB - bool bg_priority; // For sprite FIFO – the BG priority bit. For the BG FIFO – the CGB attributes priority bit + uint8_t priority; // Object priority – 0 in DMG, OAM index in CGB + bool bg_priority; // For object FIFO – the BG priority bit. For the BG FIFO – the CGB attributes priority bit } GB_fifo_item_t; #define GB_FIFO_LENGTH 16 @@ -305,6 +300,55 @@ 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; + +/* Duplicated so it can remain anonymous in GB_gameboy_t */ +typedef union { + uint16_t registers[GB_REGISTERS_16_BIT]; + struct { + uint16_t af, + bc, + de, + hl, + sp, + pc; + }; + struct { +#ifdef GB_BIG_ENDIAN + uint8_t a, f, + b, c, + d, e, + h, l; +#else + uint8_t f, a, + c, b, + e, d, + l, h; +#endif + }; +} GB_registers_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. @@ -328,31 +372,30 @@ struct GB_gameboy_internal_s { GB_SECTION(core_state, /* Registers */ - uint16_t pc; - union { - uint16_t registers[GB_REGISTERS_16_BIT]; - struct { - uint16_t af, - bc, - de, - hl, - sp; - }; - struct { + union { + uint16_t registers[GB_REGISTERS_16_BIT]; + struct { + uint16_t af, + bc, + de, + hl, + sp, + pc; + }; + struct { #ifdef GB_BIG_ENDIAN - uint8_t a, f, - b, c, - d, e, - h, l; + uint8_t a, f, + b, c, + d, e, + h, l; #else - uint8_t f, a, - c, b, - e, d, - l, h; + uint8_t f, a, + c, b, + e, d, + l, h; #endif - }; - - }; + }; + }; uint8_t ime; uint8_t interrupt_enable; uint8_t cgb_ram_bank; @@ -374,6 +417,10 @@ 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; + uint16_t address_bus; ); /* DMA and HDMA */ @@ -389,6 +436,8 @@ struct GB_gameboy_internal_s { uint16_t dma_current_src; int16_t dma_cycles; bool is_dma_restarting; + uint8_t dma_and_pattern; + bool dma_skip_write; uint8_t last_opcode_read; /* Required to emulte HDMA reads from Exxx */ bool hdma_starting; ); @@ -413,6 +462,7 @@ struct GB_gameboy_internal_s { struct { uint8_t rom_bank:8; uint8_t ram_bank:3; + bool rtc_mapped:1; } mbc3; struct { @@ -420,6 +470,22 @@ struct GB_gameboy_internal_s { uint8_t rom_bank_high:1; uint8_t ram_bank:4; } mbc5; + + struct { + uint8_t rom_bank; + uint16_t x_latch; + uint16_t y_latch; + bool latch_ready:1; + bool eeprom_do:1; + bool eeprom_di:1; + bool eeprom_clk:1; + bool eeprom_cs:1; + uint16_t eeprom_command:11; + uint16_t read_bits; + uint8_t bits_countdown:5; + bool secondary_ram_enable:1; + bool eeprom_write_enabled:1; + } mbc7; struct { uint8_t bank_low:6; @@ -432,23 +498,27 @@ struct GB_gameboy_internal_s { uint8_t rom_bank:7; uint8_t padding:1; uint8_t ram_bank:4; + uint8_t mode; + uint8_t access_index; + uint16_t minutes, days; + uint16_t alarm_minutes, alarm_days; + bool alarm_enabled; + uint8_t read; + uint8_t access_flags; } huc3; + + struct { + uint16_t rom_bank; + uint8_t ram_bank; + uint8_t mode; + } tpp1; }; 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 - uint8_t huc3_mode; - uint8_t huc3_access_index; - uint16_t huc3_minutes, huc3_days; - uint16_t huc3_alarm_minutes, huc3_alarm_days; - bool huc3_alarm_enabled; - uint8_t huc3_read; - uint8_t huc3_access_flags; - bool mbc3_rtc_mapped; + ); @@ -468,6 +538,14 @@ 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 + /* For timing of the vblank callback */ + uint32_t cycles_since_vblank_callback; + bool lcd_disabled_outside_of_vblank; + int32_t allowed_pending_cycles; + uint16_t mode3_batching_length; ); /* APU */ @@ -479,16 +557,17 @@ struct GB_gameboy_internal_s { GB_SECTION(rtc, GB_rtc_time_t rtc_real, rtc_latched; uint64_t last_rtc_second; - bool rtc_latch; + uint32_t rtc_cycles; + uint8_t tpp1_mr4; ); /* Video Display */ GB_SECTION(video, uint32_t vram_size; // Different between CGB and DMG - uint8_t cgb_vram_bank; + bool cgb_vram_bank; uint8_t oam[0xA0]; uint8_t background_palettes_data[0x40]; - uint8_t sprite_palettes_data[0x40]; + uint8_t object_palettes_data[0x40]; uint8_t position_in_line; bool stat_interrupt_line; uint8_t effective_scx; @@ -513,7 +592,6 @@ struct GB_gameboy_internal_s { uint8_t current_line; uint16_t ly_for_comparison; GB_fifo_t bg_fifo, oam_fifo; - uint8_t fetcher_x; uint8_t fetcher_y; uint16_t cycles_for_line; uint8_t current_tile; @@ -528,12 +606,11 @@ struct GB_gameboy_internal_s { uint8_t n_visible_objs; uint8_t oam_search_index; uint8_t accessed_oam_row; - uint8_t extra_penalty_for_sprite_at_0; + uint8_t extra_penalty_for_object_at_0; uint8_t mode_for_interrupt; bool lyc_interrupt_line; bool cgb_palettes_blocked; uint8_t current_lcd_line; // The LCD can go out of sync since the vsync signal is skipped in some cases. - uint32_t cycles_in_stop_mode; uint8_t object_priority; bool oam_ppu_blocked; bool vram_ppu_blocked; @@ -548,6 +625,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 */ @@ -573,18 +651,28 @@ struct GB_gameboy_internal_s { /* I/O */ uint32_t *screen; uint32_t background_palettes_rgb[0x20]; - uint32_t sprite_palettes_rgb[0x20]; + uint32_t object_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]; + double accelerometer_x, accelerometer_y; GB_border_mode_t border_mode; GB_sgb_border_t borrowed_border; bool tried_loading_sgb_border; bool has_sgb_border; - + bool objects_disabled; + bool background_disabled; + bool joyp_accessed; + bool illegal_inputs_allowed; + /* Timing */ uint64_t last_sync; uint64_t cycles_since_last_sync; // In 8MHz units + GB_rtc_mode_t rtc_mode; + uint32_t rtc_second_length; + uint32_t clock_rate; + uint32_t unmultiplied_clock_rate; /* Audio */ GB_apu_output_t apu_output; @@ -608,16 +696,13 @@ struct GB_gameboy_internal_s { GB_icd_vreset_callback_t icd_hreset_callback; GB_icd_vreset_callback_t icd_vreset_callback; GB_read_memory_callback_t read_memory_callback; + GB_write_memory_callback_t write_memory_callback; GB_boot_rom_load_callback_t boot_rom_load_callback; 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; + GB_execution_callback_t execution_callback; + GB_lcd_line_callback_t lcd_line_callback; /*** Debugger ***/ volatile bool debug_stopped, debug_disable; @@ -654,7 +739,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,12 +782,16 @@ struct GB_gameboy_internal_s { /* Temporary state */ bool wx_just_changed; + bool tile_sel_glitch; + bool disable_oam_corruption; // For safe memory reads + + GB_gbs_header_t gbs_header; ); }; #ifndef GB_INTERNAL struct GB_gameboy_s { - char __internal[sizeof(struct GB_gameboy_internal_s)]; + alignas(struct GB_gameboy_internal_s) uint8_t __internal[sizeof(struct GB_gameboy_internal_s)]; }; #endif @@ -710,7 +804,8 @@ __attribute__((__format__ (__printf__, fmtarg, firstvararg))) void GB_init(GB_gameboy_t *gb, GB_model_t model); bool GB_is_inited(GB_gameboy_t *gb); -bool GB_is_cgb(GB_gameboy_t *gb); +bool GB_is_cgb(const GB_gameboy_t *gb); +bool GB_is_cgb_in_cgb_mode(GB_gameboy_t *gb); bool GB_is_sgb(GB_gameboy_t *gb); // Returns true if the model is SGB or SGB2 bool GB_is_hle_sgb(GB_gameboy_t *gb); // Returns true if the model is SGB or SGB2 and the SFC/SNES side is HLE'd GB_model_t GB_get_model(GB_gameboy_t *gb); @@ -740,18 +835,20 @@ typedef enum { /* Returns a mutable pointer to various hardware memories. If that memory is banked, the current bank is returned at *bank, even if only a portion of the memory is banked. */ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *size, uint16_t *bank); +GB_registers_t *GB_get_registers(GB_gameboy_t *gb); 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 +866,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); @@ -782,7 +878,11 @@ void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_ca /* Called when a new boot ROM is needed. The callback should call GB_load_boot_rom or GB_load_boot_rom_from_buffer */ void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t callback); +void GB_set_execution_callback(GB_gameboy_t *gb, GB_execution_callback_t callback); +void GB_set_lcd_line_callback(GB_gameboy_t *gb, GB_lcd_line_callback_t callback); + void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette); +const GB_palette_t *GB_get_palette(GB_gameboy_t *gb); /* These APIs are used when using internal clock */ void GB_set_serial_transfer_bit_start_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_start_callback_t callback); @@ -796,6 +896,12 @@ 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 + +/* For cartridges motion controls */ +bool GB_has_accelerometer(GB_gameboy_t *gb); +// In units of g (gravity's acceleration). +// Values within ±4 recommended +void GB_set_accelerometer_values(GB_gameboy_t *gb, double x, double y); /* For integration with SFC/SNES emulators */ void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback); @@ -803,9 +909,8 @@ void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callbac 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 +918,14 @@ 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); + +#ifdef GB_INTERNAL +internal void GB_borrow_sgb_border(GB_gameboy_t *gb); +internal void GB_update_clock_rate(GB_gameboy_t *gb); +#endif + #endif /* GB_h */ diff --git a/Core/gb_struct_def.h b/Core/gb_struct_def.h deleted file mode 100644 index 0e0ebd1..0000000 --- a/Core/gb_struct_def.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef gb_struct_def_h -#define gb_struct_def_h -struct GB_gameboy_s; -typedef struct GB_gameboy_s GB_gameboy_t; -#endif diff --git a/Core/graphics/mgb_border.inc b/Core/graphics/mgb_border.inc new file mode 100644 index 0000000..f19ed8a --- /dev/null +++ b/Core/graphics/mgb_border.inc @@ -0,0 +1,477 @@ +static const uint16_t palette[] = { + 0x0000, 0x0000, 0x0011, 0x001A, 0x39CE, 0x6B5A, 0x739C, 0x5265, + 0x3DC5, 0x2924, 0x18A4, 0x20E6, 0x2D49, 0x1484, 0x5694, 0x20EC, +}; + + +static const uint16_t tilemap[] = { + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x0010, 0x0011, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, + 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, + 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, + 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x4011, 0x4010, 0x000F, + 0x000F, 0x0013, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x4013, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0016, 0x0017, 0x0017, + 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, + 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, + 0x0017, 0x0017, 0x4016, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0019, 0x001A, 0x4019, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x8019, 0x001B, 0xC019, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x001C, 0x001D, 0x001D, + 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, + 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, + 0x001D, 0x001D, 0x401C, 0x0014, 0x0014, 0x0014, 0x001E, 0x000F, + 0x000F, 0x0015, 0x0014, 0x001F, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0030, 0x0031, 0x000F, + 0x000F, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, + 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, 0x0040, + 0x0041, 0x0042, 0x0043, 0x0044, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0045, 0x0046, 0x000F, 0x000F, + 0x000F, 0x0047, 0x0048, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, + 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, + 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, + 0x0049, 0x0049, 0x004A, 0x004B, 0x004C, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, +}; + + + +const uint8_t tiles[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x3D, + 0x42, 0x7F, 0x81, 0xFF, 0x01, 0xFD, 0x01, + 0xFD, 0x01, 0xFF, 0x03, 0xFF, 0x03, 0xFF, + 0xFF, 0xBC, 0x7F, 0xFD, 0xFE, 0xFD, 0xFE, + 0xFD, 0xFE, 0xFD, 0xFE, 0xFF, 0xFC, 0xFF, + 0xFC, 0xFF, 0x00, 0xBF, 0x41, 0xFE, 0xC0, + 0xBF, 0xC1, 0xFF, 0x81, 0x7D, 0x03, 0x7F, + 0x01, 0x7F, 0x01, 0xFF, 0xFF, 0x3E, 0xFF, + 0xBE, 0x7F, 0xBF, 0x7E, 0xFF, 0x7E, 0x7D, + 0xFE, 0x7D, 0xFE, 0x7D, 0xFE, 0xFF, 0x00, + 0xFF, 0x00, 0xBF, 0x83, 0xBF, 0x87, 0xFC, + 0x8D, 0xED, 0x8E, 0xDB, 0xF8, 0xBF, 0xD8, + 0xFF, 0xFF, 0x3E, 0xFF, 0xBB, 0x7C, 0xB7, + 0x78, 0xAC, 0x73, 0xAD, 0x73, 0x9B, 0x67, + 0x9B, 0x67, 0xFF, 0x00, 0xB7, 0x08, 0xFF, + 0xF8, 0x3F, 0x38, 0xFF, 0x08, 0xFE, 0x01, + 0x87, 0x00, 0xFB, 0x78, 0xFF, 0xFF, 0x07, + 0xFF, 0xFB, 0x07, 0x3B, 0xC7, 0xE7, 0xFF, + 0xFE, 0xFF, 0x82, 0xFF, 0xFA, 0x87, 0xFF, + 0x00, 0xFE, 0x81, 0x5F, 0x40, 0xDE, 0xC0, + 0xFE, 0xC0, 0xE0, 0xDE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1E, 0xFF, 0x5E, 0xBF, + 0xDE, 0x3F, 0xDE, 0x3F, 0xC0, 0x3F, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xCF, 0x30, + 0xDF, 0xEF, 0xFF, 0xCF, 0xFF, 0xC1, 0xBD, + 0x81, 0xBD, 0x81, 0xFF, 0x83, 0xFF, 0xFF, + 0x00, 0xFF, 0xCF, 0x30, 0xEF, 0x30, 0xFD, + 0x3E, 0xBD, 0x7E, 0xBD, 0x7E, 0xBF, 0x7C, + 0xFF, 0x00, 0xFF, 0x08, 0xF7, 0xF0, 0xFF, + 0xF0, 0xBF, 0xC0, 0xFF, 0x80, 0x7F, 0x00, + 0x7F, 0x00, 0xFF, 0xFF, 0x07, 0xFF, 0xF7, + 0x0F, 0xF7, 0x0F, 0xBF, 0x7F, 0xFF, 0x7F, + 0x7F, 0xFF, 0x7F, 0xFF, 0xFB, 0x07, 0xFF, + 0x03, 0xFF, 0x03, 0xFB, 0x03, 0xFB, 0x03, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, + 0xFC, 0xFB, 0xFC, 0xFB, 0xFC, 0xFB, 0xFC, + 0xFB, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFD, 0x01, 0xFD, 0x81, 0xFF, 0x0B, + 0xF7, 0xF3, 0xFB, 0xF7, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0x7D, 0xFE, 0x7D, 0xFE, + 0x07, 0xFC, 0xF7, 0x0C, 0xF3, 0x0C, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, + 0x7B, 0x38, 0x7C, 0x1D, 0xFF, 0x0F, 0xFB, + 0x0B, 0xFD, 0x03, 0xFF, 0x00, 0xFF, 0x00, + 0xDB, 0x67, 0x5B, 0xE7, 0x7C, 0xE3, 0x6F, + 0xF0, 0x73, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x9E, 0x18, 0xFE, 0x3C, 0x5A, + 0xDC, 0xFF, 0xF9, 0xED, 0xE3, 0xBF, 0xC0, + 0xFF, 0x00, 0xFF, 0x00, 0x9A, 0xE7, 0xDA, + 0xE7, 0x1A, 0xE7, 0xFF, 0x06, 0xE5, 0x1E, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, + 0xFD, 0xBF, 0x81, 0xBF, 0x81, 0xBD, 0x81, + 0xFD, 0x81, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xC1, 0x3E, 0xBD, 0x7E, 0xBD, 0x7E, + 0xBD, 0x7E, 0xBD, 0x7E, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xBB, 0xC7, + 0xFF, 0x83, 0xFF, 0x83, 0x7B, 0x03, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xBF, 0x7C, + 0xBB, 0x7C, 0xFB, 0x7C, 0xFB, 0x7C, 0x7B, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, + 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFE, 0x00, 0xF9, 0x00, + 0xF7, 0x00, 0xEE, 0x00, 0xDD, 0x04, 0xDF, + 0x04, 0xBF, 0x08, 0xFF, 0x00, 0xFF, 0x01, + 0xFF, 0x07, 0xFF, 0x0F, 0xFF, 0x1F, 0xFB, + 0x3F, 0xFB, 0x3F, 0xF7, 0x7F, 0x80, 0x00, + 0x7F, 0x00, 0xFF, 0x00, 0x80, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, + 0x08, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, + 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, + 0x10, 0xF7, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, + 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, + 0x7F, 0xEF, 0x7F, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, + 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, + 0xFF, 0x10, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, + 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, + 0xEF, 0x7F, 0xEF, 0x7F, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFE, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, + 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xC3, 0x7E, + 0xBD, 0xFF, 0x66, 0xFF, 0x7E, 0xFF, 0x7E, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC3, 0xFF, 0x81, 0xC3, 0x18, 0x81, 0x00, + 0x00, 0x00, 0x00, 0x7E, 0xFF, 0x3C, 0xFF, + 0x00, 0x7E, 0x81, 0xBD, 0xC3, 0x42, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x81, 0x00, 0xC3, 0x81, 0xFF, + 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x01, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x08, 0xFD, 0x12, 0xFD, 0x12, + 0xFD, 0x12, 0xFD, 0x12, 0xFB, 0x24, 0xFB, + 0x24, 0xFB, 0x24, 0xF7, 0xFE, 0xEF, 0xFC, + 0xEF, 0xFC, 0xEF, 0xFC, 0xEF, 0xFC, 0xDF, + 0xF8, 0xDF, 0xF8, 0xDF, 0xF8, 0xFF, 0x00, + 0xF0, 0x1E, 0xC0, 0x3F, 0x8D, 0x72, 0x0E, + 0xF3, 0x8F, 0xF0, 0x01, 0xFC, 0xA0, 0x1E, + 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xE0, 0xFF, + 0xC0, 0x7C, 0x9F, 0x7F, 0x9F, 0x7F, 0x87, + 0xFF, 0xC0, 0xFF, 0x00, 0xFC, 0x00, 0x78, + 0x87, 0x78, 0x87, 0xF0, 0x07, 0xF8, 0x07, + 0xE2, 0x0F, 0xE2, 0x1C, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFB, 0xFC, 0x7F, 0xFC, 0xFF, 0xF8, + 0xFF, 0xF2, 0xFD, 0xF3, 0xFF, 0xE6, 0xFF, + 0x00, 0x7C, 0x02, 0xFC, 0x01, 0x3C, 0xC3, + 0x3C, 0x83, 0x3E, 0xC1, 0x3C, 0xC3, 0x18, + 0xC7, 0xFF, 0xFF, 0xFD, 0xFE, 0xFF, 0x7C, + 0xBF, 0x7E, 0xFF, 0x3E, 0xFF, 0x7C, 0xFF, + 0x3C, 0xFB, 0x3C, 0xFF, 0x00, 0x1E, 0x01, + 0x1E, 0xE1, 0x1E, 0xE3, 0x1C, 0xF3, 0x0C, + 0xE7, 0x08, 0xF7, 0x19, 0x6F, 0xFF, 0xFF, + 0xFE, 0x3F, 0xFF, 0x3F, 0xFD, 0x1E, 0xEF, + 0x1E, 0xFB, 0x8C, 0xFF, 0xDD, 0xF6, 0x49, + 0xFF, 0x00, 0x18, 0x14, 0x08, 0xE3, 0x08, + 0xE3, 0x18, 0xF7, 0x1D, 0xE2, 0x18, 0xE7, + 0xB8, 0x47, 0xFF, 0xFF, 0xEB, 0x1C, 0xFF, + 0x0C, 0xFF, 0x18, 0xEF, 0x1C, 0xFF, 0x98, + 0xFF, 0x98, 0xFF, 0x18, 0xFF, 0x00, 0x06, + 0x05, 0x02, 0xF8, 0x02, 0xF9, 0xFE, 0x01, + 0xFF, 0x00, 0x06, 0xF9, 0x04, 0xF3, 0xFF, + 0xFF, 0xFA, 0x07, 0xFF, 0x02, 0xFF, 0x07, + 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x0E, 0xFD, + 0x06, 0xFF, 0x00, 0x03, 0x00, 0x01, 0xFF, + 0x30, 0xCF, 0x70, 0x8F, 0x30, 0xC6, 0x21, + 0xDC, 0x01, 0xFE, 0xFF, 0xFF, 0xFF, 0x0F, + 0xFE, 0x01, 0xFF, 0x01, 0xF7, 0x39, 0xFF, + 0x79, 0xFF, 0x01, 0xFF, 0x03, 0xFF, 0x00, + 0xF8, 0x01, 0xF0, 0x1F, 0xE1, 0x3E, 0x83, + 0x78, 0x87, 0x30, 0xCF, 0x30, 0x8F, 0x70, + 0xFF, 0xFF, 0xFF, 0xFD, 0xEF, 0xF8, 0xDF, + 0xE0, 0xBF, 0xC3, 0xFF, 0x87, 0xFF, 0x8F, + 0xFF, 0x9F, 0xFF, 0x00, 0x3C, 0xA0, 0x0C, + 0xE1, 0x07, 0xF0, 0x86, 0x38, 0xC7, 0x3C, + 0xC3, 0x18, 0xC7, 0x3C, 0xFF, 0xFF, 0xDF, + 0xBE, 0xFF, 0x0C, 0xFF, 0x06, 0xFF, 0x87, + 0xFB, 0xE7, 0xFF, 0xC7, 0xFB, 0xE7, 0xFF, + 0x00, 0x7C, 0x40, 0x78, 0x83, 0x39, 0xEE, + 0x19, 0xE7, 0x81, 0x7C, 0x03, 0x78, 0xC7, + 0x38, 0xFF, 0xFF, 0xBF, 0x7E, 0xFF, 0x38, + 0xD7, 0x38, 0xFE, 0x31, 0xFF, 0x13, 0xFF, + 0x83, 0xFF, 0x8F, 0xFF, 0x00, 0x3C, 0x43, + 0x7C, 0x83, 0xFC, 0x03, 0xFC, 0x03, 0xFC, + 0x03, 0xFD, 0x02, 0xFC, 0x03, 0xFF, 0xFF, + 0xBF, 0x7C, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, + 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, 0xFD, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0xA3, 0xFD, 0xB6, 0x8C, 0x3A, 0x8B, 0x7C, + 0x99, 0x62, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0xF8, 0x4B, 0xFC, 0xF7, 0xCC, + 0xF3, 0xCD, 0xFF, 0xCB, 0xFF, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x90, 0x3F, 0xD8, 0x46, + 0x09, 0xF6, 0x0D, 0xF1, 0x12, 0xF4, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xCF, 0x78, + 0xBF, 0xD9, 0x7F, 0x9F, 0xFE, 0x9B, 0xEF, + 0x9B, 0xFF, 0x00, 0x00, 0xFC, 0x02, 0xFC, + 0x46, 0x59, 0x13, 0xAC, 0x82, 0x68, 0x07, + 0xFC, 0x14, 0xE8, 0xFF, 0xFF, 0xFF, 0x03, + 0xFF, 0x02, 0xBF, 0x73, 0x5F, 0xB6, 0xFF, + 0x23, 0xFB, 0x07, 0xFF, 0x26, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x03, 0x9F, 0x21, + 0x55, 0x48, 0xB7, 0x8F, 0x60, 0x80, 0x6F, + 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFC, + 0x27, 0xBA, 0xCD, 0x7F, 0x9D, 0xFF, 0x8F, + 0xF7, 0xD8, 0xFF, 0x00, 0x00, 0xFF, 0x0C, + 0xF3, 0x18, 0xF3, 0xD8, 0x2F, 0x90, 0x67, + 0xB0, 0x4F, 0x10, 0xEF, 0xFF, 0xFF, 0xFF, + 0x08, 0xFF, 0x18, 0xEF, 0xBC, 0xF7, 0xBC, + 0xFF, 0xD0, 0xFF, 0xD8, 0xFF, 0x30, 0xFF, + 0x00, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, + 0x80, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, + 0x7F, 0xFF, 0x7F, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, + 0x02, 0xFF, 0x04, 0xFF, 0x08, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFF, 0xFD, 0xFF, 0xFB, 0xFF, 0xF7, 0xFF, + 0xF7, 0x48, 0xF7, 0x48, 0xEF, 0x90, 0xEF, + 0x90, 0xDF, 0x20, 0xBF, 0x40, 0xBF, 0x40, + 0x7F, 0x80, 0xBF, 0xF0, 0xBF, 0xF0, 0x7F, + 0xE0, 0x7F, 0xE0, 0xFF, 0xC0, 0xFF, 0x80, + 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x10, 0xFF, + 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, + 0xFF, 0x10, 0xFF, 0x10, 0xBF, 0x48, 0xEF, + 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, + 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xF7, + 0x3F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, + 0xF8, 0x07, 0x00, 0x57, 0x01, 0xFF, 0x05, + 0xF8, 0x87, 0x48, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xF8, 0xFF, 0xFC, 0xEF, 0x99, 0xFE, + 0x01, 0xFF, 0x83, 0xB7, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xC0, 0x3F, 0x80, 0x7F, 0x80, + 0x7F, 0x0F, 0x70, 0x8F, 0x70, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xE7, 0xBF, + 0xC0, 0xFF, 0xCF, 0xFF, 0x9F, 0xEF, 0x1F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x18, + 0xE3, 0x18, 0xE3, 0x98, 0x77, 0x08, 0x67, + 0x1D, 0xE2, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x3C, 0xFF, 0x18, 0xEF, 0x1C, + 0xFF, 0x0C, 0x7F, 0x88, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x01, 0x3E, 0xC2, 0xFF, + 0xC2, 0x3C, 0xE2, 0x18, 0xC6, 0x39, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x8B, + 0x3C, 0xC3, 0xFF, 0xC7, 0xFF, 0xC6, 0xFF, + 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x18, 0xEF, 0x10, 0xE6, 0x10, 0xC6, 0x30, + 0xEF, 0x30, 0xCF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xF7, 0x39, 0xFF, 0x39, 0xFF, + 0x10, 0xDF, 0x38, 0xFF, 0x38, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0x09, 0xFC, + 0x01, 0x0C, 0x0B, 0x04, 0xFB, 0x06, 0xF1, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xF7, + 0x0E, 0xFF, 0xFC, 0xF7, 0x0E, 0xFF, 0x0E, + 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xCE, 0x70, 0xCF, 0x70, 0xFE, + 0x01, 0xFE, 0x0B, 0xF0, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x69, 0xB7, 0x79, + 0x8F, 0x79, 0xFF, 0x03, 0xFF, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0x70, + 0x87, 0x78, 0x88, 0x72, 0x80, 0x7F, 0xC0, + 0x3F, 0xFC, 0x05, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x9F, 0xF7, 0x8F, 0xFD, 0xC7, 0xBF, + 0xC0, 0xDF, 0xF0, 0xFA, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE7, 0x18, 0x87, 0x38, 0x17, + 0xE8, 0x1F, 0xF0, 0x3F, 0xC0, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xC7, 0xFF, + 0x8F, 0xF7, 0x8F, 0xEF, 0x1F, 0xFF, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, + 0x78, 0x8F, 0x70, 0xDF, 0x20, 0x8F, 0x70, + 0x8F, 0x70, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xF7, 0xCF, 0xFF, 0xCF, 0xFF, 0x8F, + 0xFF, 0x9F, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFC, 0x02, 0xFD, 0x03, + 0xFD, 0x02, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFC, + 0xFE, 0xFD, 0xFF, 0xFD, 0xFF, 0xFD, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x9F, 0x30, 0xA0, 0x9E, 0x00, 0x7F, 0x00, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xEF, 0xF9, 0x6F, 0xF8, 0xFF, + 0x00, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x90, 0xCF, 0xE1, + 0x1E, 0x40, 0xBF, 0x00, 0xFF, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7C, + 0xDB, 0xFF, 0xF3, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x3D, 0x82, 0x86, 0x79, 0x80, 0x7F, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0xA6, 0xBF, 0xEC, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x88, 0x27, + 0xD6, 0xA0, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0xDF, 0x7F, 0xCE, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x18, 0x47, 0x10, 0xC7, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x10, 0xFF, + 0x18, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, + 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, + 0x02, 0xFF, 0x0C, 0xFF, 0x10, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xEF, 0xFF, + 0xFE, 0x11, 0xFD, 0x22, 0xFB, 0x44, 0xF7, + 0x88, 0xEF, 0x10, 0xDF, 0x20, 0xBF, 0x40, + 0x7F, 0x80, 0xEF, 0xFE, 0xDF, 0xFC, 0xBF, + 0xF8, 0x7F, 0xF0, 0xFF, 0xE0, 0xFF, 0xC0, + 0xFF, 0x80, 0xFF, 0x00, 0xBF, 0x48, 0xDF, + 0x24, 0xDF, 0x22, 0xEF, 0x11, 0xF7, 0x08, + 0xF9, 0x06, 0xFE, 0x01, 0xFF, 0x00, 0xF7, + 0x3F, 0xFB, 0x1F, 0xFD, 0x1F, 0xFE, 0x0F, + 0xFF, 0x07, 0xFF, 0x01, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x80, 0xFF, 0x7F, 0xFF, 0x00, 0x7F, + 0x80, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x7F, 0xFF, 0x80, 0xFF, 0xFF, + 0xFF, 0xFF, 0x7F, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x03, 0xFF, 0xFC, 0xFF, 0x00, + 0xFC, 0x03, 0x03, 0xFC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x03, 0xFF, 0x1C, 0xFF, 0xE0, + 0xFC, 0x03, 0xE3, 0x1C, 0x1F, 0xE0, 0xFF, + 0x00, 0xFF, 0xFF, 0xFC, 0xFF, 0xE3, 0xFF, + 0x1F, 0xFF, 0xFF, 0xFC, 0xFF, 0xE0, 0xFF, + 0x00, 0xFF, 0x00, 0xFC, 0xE3, 0xF3, 0x0C, + 0xEF, 0x10, 0x1F, 0xE0, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x1F, 0xFC, + 0xFF, 0xF0, 0xFF, 0xE0, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, +}; diff --git a/Core/joypad.c b/Core/joypad.c index b8d4fdb..db50f66 100644 --- a/Core/joypad.c +++ b/Core/joypad.c @@ -12,7 +12,7 @@ void GB_update_joyp(GB_gameboy_t *gb) previous_state = gb->io_registers[GB_IO_JOYP] & 0xF; key_selection = (gb->io_registers[GB_IO_JOYP] >> 4) & 3; gb->io_registers[GB_IO_JOYP] &= 0xF0; - uint8_t current_player = gb->sgb? (gb->sgb->current_player & (gb->sgb->player_count - 1) & 3) : 0; + uint8_t current_player = gb->sgb? gb->sgb->current_player : 0; switch (key_selection) { case 3: if (gb->sgb && gb->sgb->player_count > 1) { @@ -30,11 +30,13 @@ void GB_update_joyp(GB_gameboy_t *gb) gb->io_registers[GB_IO_JOYP] |= (!gb->keys[current_player][i]) << i; } /* Forbid pressing two opposing keys, this breaks a lot of games; even if it's somewhat possible. */ - if (!(gb->io_registers[GB_IO_JOYP] & 1)) { - gb->io_registers[GB_IO_JOYP] |= 2; - } - if (!(gb->io_registers[GB_IO_JOYP] & 4)) { - gb->io_registers[GB_IO_JOYP] |= 8; + if (likely(!gb->illegal_inputs_allowed)) { + if (!(gb->io_registers[GB_IO_JOYP] & 1)) { + gb->io_registers[GB_IO_JOYP] |= 2; + } + if (!(gb->io_registers[GB_IO_JOYP] & 4)) { + gb->io_registers[GB_IO_JOYP] |= 8; + } } break; @@ -51,8 +53,7 @@ void GB_update_joyp(GB_gameboy_t *gb) } break; - default: - break; + nodefault; } /* Todo: This assumes the keys *always* bounce, which is incorrect when emulating an SGB */ @@ -90,3 +91,48 @@ void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned play gb->keys[player][index] = pressed; GB_update_joyp(gb); } + +void GB_set_key_mask(GB_gameboy_t *gb, GB_key_mask_t mask) +{ + memset(gb->keys, 0, sizeof(gb->keys)); + bool *key = &gb->keys[0][0]; + while (mask) { + if (mask & 1) { + *key = true; + } + mask >>= 1; + key++; + } + + GB_update_joyp(gb); +} + +void GB_set_key_mask_for_player(GB_gameboy_t *gb, GB_key_mask_t mask, unsigned player) +{ + memset(gb->keys[player], 0, sizeof(gb->keys[player])); + bool *key = gb->keys[player]; + while (mask) { + if (mask & 1) { + *key = true; + } + mask >>= 1; + key++; + } + + GB_update_joyp(gb); +} + +bool GB_get_joyp_accessed(GB_gameboy_t *gb) +{ + return gb->joyp_accessed; +} + +void GB_clear_joyp_accessed(GB_gameboy_t *gb) +{ + gb->joyp_accessed = false; +} + +void GB_set_allow_illegal_inputs(GB_gameboy_t *gb, bool allow) +{ + gb->illegal_inputs_allowed = allow; +} diff --git a/Core/joypad.h b/Core/joypad.h index 21fad53..21574e0 100644 --- a/Core/joypad.h +++ b/Core/joypad.h @@ -1,6 +1,6 @@ #ifndef joypad_h #define joypad_h -#include "gb_struct_def.h" +#include "defs.h" #include typedef enum { @@ -15,11 +15,31 @@ typedef enum { GB_KEY_MAX } GB_key_t; +typedef enum { + GB_KEY_RIGHT_MASK = 1 << GB_KEY_RIGHT, + GB_KEY_LEFT_MASK = 1 << GB_KEY_LEFT, + GB_KEY_UP_MASK = 1 << GB_KEY_UP, + GB_KEY_DOWN_MASK = 1 << GB_KEY_DOWN, + GB_KEY_A_MASK = 1 << GB_KEY_A, + GB_KEY_B_MASK = 1 << GB_KEY_B, + GB_KEY_SELECT_MASK = 1 << GB_KEY_SELECT, + GB_KEY_START_MASK = 1 << GB_KEY_START, +} GB_key_mask_t; + +// For example, for player 2's (0-based; logical player 3) A button, use GB_MASK_FOR_PLAYER(GB_KEY_A_MASK, 2) +#define GB_MASK_FOR_PLAYER(mask, player) ((x) << (player * 8)) + void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed); void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned player, bool pressed); +void GB_set_key_mask(GB_gameboy_t *gb, GB_key_mask_t mask); +void GB_set_key_mask_for_player(GB_gameboy_t *gb, GB_key_mask_t mask, unsigned player); void GB_icd_set_joyp(GB_gameboy_t *gb, uint8_t value); +bool GB_get_joyp_accessed(GB_gameboy_t *gb); +void GB_clear_joyp_accessed(GB_gameboy_t *gb); +void GB_set_allow_illegal_inputs(GB_gameboy_t *gb, bool allow); + #ifdef GB_INTERNAL -void GB_update_joyp(GB_gameboy_t *gb); +internal void GB_update_joyp(GB_gameboy_t *gb); #endif #endif /* joypad_h */ diff --git a/Core/mbc.c b/Core/mbc.c index 2259681..5ade9aa 100644 --- a/Core/mbc.c +++ b/Core/mbc.c @@ -34,6 +34,8 @@ const GB_cartridge_t GB_cart_defs[256] = { { GB_MBC5 , GB_STANDARD_MBC, false, false, false, true }, // 1Ch MBC5+RUMBLE { GB_MBC5 , GB_STANDARD_MBC, true , false, false, true }, // 1Dh MBC5+RUMBLE+RAM { GB_MBC5 , GB_STANDARD_MBC, true , true , false, true }, // 1Eh MBC5+RUMBLE+RAM+BATTERY + [0x22] = + { GB_MBC7 , GB_STANDARD_MBC, true, true, false, false}, // 22h MBC7+ACCEL+EEPROM [0xFC] = { GB_MBC5 , GB_CAMERA , true , true , false, false}, // FCh POCKET CAMERA { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // FDh BANDAI TAMA5 (Todo: Not supported) @@ -75,6 +77,7 @@ void GB_update_mbc_mappings(GB_gameboy_t *gb) gb->mbc_rom_bank++; } break; + nodefault; } break; case GB_MBC2: @@ -97,6 +100,9 @@ void GB_update_mbc_mappings(GB_gameboy_t *gb) gb->mbc_rom_bank = gb->mbc5.rom_bank_low | (gb->mbc5.rom_bank_high << 8); gb->mbc_ram_bank = gb->mbc5.ram_bank; break; + case GB_MBC7: + gb->mbc_rom_bank = gb->mbc7.rom_bank; + break; case GB_HUC1: if (gb->huc1.mode == 0) { gb->mbc_rom_bank = gb->huc1.bank_low | (gb->mbc1.bank_high << 6); @@ -111,12 +117,25 @@ 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; + nodefault; } } 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 +144,25 @@ 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; + gb->mbc_ram_size = 0; + } 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_MBC7) { + gb->mbc_ram_size = 0x100; + } + 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 +172,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); } @@ -164,4 +197,12 @@ void GB_configure_cart(GB_gameboy_t *gb) if (gb->cartridge_type->mbc_type == GB_MBC5) { gb->mbc5.rom_bank_low = 1; } + + /* Initial MBC7 state */ + if (gb->cartridge_type->mbc_type == GB_MBC7) { + gb->mbc7.x_latch = gb->mbc7.y_latch = 0x8000; + gb->mbc7.latch_ready = true; + gb->mbc7.read_bits = -1; + gb->mbc7.eeprom_do = true; + } } diff --git a/Core/mbc.h b/Core/mbc.h index 6a23300..7e6cc82 100644 --- a/Core/mbc.h +++ b/Core/mbc.h @@ -1,6 +1,6 @@ #ifndef MBC_h #define MBC_h -#include "gb_struct_def.h" +#include "defs.h" #include typedef struct { @@ -10,8 +10,10 @@ typedef struct { GB_MBC2, GB_MBC3, GB_MBC5, + GB_MBC7, GB_HUC1, GB_HUC3, + GB_TPP1, } mbc_type; enum { GB_STANDARD_MBC, @@ -25,8 +27,8 @@ typedef struct { #ifdef GB_INTERNAL extern const GB_cartridge_t GB_cart_defs[256]; -void GB_update_mbc_mappings(GB_gameboy_t *gb); -void GB_configure_cart(GB_gameboy_t *gb); +internal void GB_update_mbc_mappings(GB_gameboy_t *gb); +internal void GB_configure_cart(GB_gameboy_t *gb); #endif #endif /* MBC_h */ diff --git a/Core/memory.c b/Core/memory.c index f73209e..913fb68 100644 --- a/Core/memory.c +++ b/Core/memory.c @@ -2,17 +2,16 @@ #include #include "gb.h" -typedef uint8_t GB_read_function_t(GB_gameboy_t *gb, uint16_t addr); -typedef void GB_write_function_t(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +typedef uint8_t read_function_t(GB_gameboy_t *gb, uint16_t addr); +typedef void write_function_t(GB_gameboy_t *gb, uint16_t addr, uint8_t value); typedef enum { GB_BUS_MAIN, /* In DMG: Cart and RAM. In CGB: Cart only */ GB_BUS_RAM, /* In CGB only. */ GB_BUS_VRAM, - GB_BUS_INTERNAL, /* Anything in highram. Might not be the most correct name. */ -} GB_bus_t; +} bus_t; -static GB_bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr) +static bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr) { if (addr < 0x8000) { return GB_BUS_MAIN; @@ -23,39 +22,84 @@ static GB_bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr) if (addr < 0xC000) { return GB_BUS_MAIN; } - if (addr < 0xFE00) { - return GB_is_cgb(gb)? GB_BUS_RAM : GB_BUS_MAIN; - } - return GB_BUS_INTERNAL; + return GB_is_cgb(gb)? GB_BUS_RAM : GB_BUS_MAIN; } -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) { + GB_display_sync(gb); 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,61 +107,165 @@ 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->model == GB_MODEL_MGB) { + /* TODO: This is rather simplified, research further */ + oam_bug_tertiary_read_corruption(gb, bitwise_glitch_tertiary_read_3); + } + else 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); + } + else if (gb->model == GB_MODEL_MGB && gb->accessed_oam_row == 0x40) { + 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) { - if (!gb->dma_steps_left || (gb->dma_cycles < 0 && !gb->is_dma_restarting) || addr >= 0xFE00) return false; + if (!gb->dma_steps_left || (gb->dma_cycles < 0 && !gb->is_dma_restarting) || addr >= 0xfe00) return false; + if (addr >= 0xfe00) return false; + if (gb->dma_current_src == addr) return false; // Shortcut for DMA access flow + if (gb->dma_current_src > 0xe000 && (gb->dma_current_src & ~0x2000) == addr) return false; + if (GB_is_cgb(gb)) { + if (addr >= 0xe000) { + return bus_for_addr(gb, gb->dma_current_src) != GB_BUS_VRAM; + } + if (gb->dma_current_src >= 0xe000) { + return bus_for_addr(gb, addr) != GB_BUS_VRAM; + } + } 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) { @@ -143,10 +291,11 @@ static uint8_t read_mbc_rom(GB_gameboy_t *gb, uint16_t addr) static uint8_t read_vram(GB_gameboy_t *gb, uint16_t addr) { - if (gb->vram_read_blocked) { + GB_display_sync(gb); + if (unlikely(gb->vram_read_blocked)) { return 0xFF; } - if (gb->display_state == 22 && GB_is_cgb(gb) && !gb->cgb_double_speed) { + if (unlikely(gb->display_state == 22 && GB_is_cgb(gb) && !gb->cgb_double_speed)) { if (addr & 0x1000) { addr = gb->last_tile_index_address; } @@ -158,24 +307,44 @@ static uint8_t read_vram(GB_gameboy_t *gb, uint16_t addr) addr = gb->last_tile_data_address; } } - return gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000]; + return gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)]; +} + +static uint8_t read_mbc7_ram(GB_gameboy_t *gb, uint16_t addr) +{ + if (!gb->mbc_ram_enable || !gb->mbc7.secondary_ram_enable) return 0xFF; + if (addr >= 0xB000) return 0xFF; + switch ((addr >> 4) & 0xF) { + case 2: return gb->mbc7.x_latch; + case 3: return gb->mbc7.x_latch >> 8; + case 4: return gb->mbc7.y_latch; + case 5: return gb->mbc7.y_latch >> 8; + case 6: return 0; + case 8: return gb->mbc7.eeprom_do | (gb->mbc7.eeprom_di << 1) | + (gb->mbc7.eeprom_clk << 6) | (gb->mbc7.eeprom_cs << 7); + } + return 0xFF; } static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) { + if (gb->cartridge_type->mbc_type == GB_MBC7) { + return read_mbc7_ram(gb, addr); + } + if (gb->cartridge_type->mbc_type == GB_HUC3) { - switch (gb->huc3_mode) { + switch (gb->huc3.mode) { case 0xC: // RTC read - if (gb->huc3_access_flags == 0x2) { + if (gb->huc3.access_flags == 0x2) { return 1; } - return gb->huc3_read; + return gb->huc3.read; 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); + GB_log(gb, "Unsupported HuC-3 mode %x read: %04x\n", gb->huc3.mode, addr); return 1; // TODO: What happens in this case? case 0: // TODO: R/O RAM? (or is it disabled?) case 0xA: // RAM @@ -183,7 +352,26 @@ 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; + nodefault; + } + 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 +379,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 +409,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)]; @@ -234,6 +431,37 @@ static uint8_t read_banked_ram(GB_gameboy_t *gb, uint16_t addr) return gb->ram[(addr & 0x0FFF) + gb->cgb_ram_bank * 0x1000]; } +static inline void sync_ppu_if_needed(GB_gameboy_t *gb, uint8_t register_accessed) +{ + switch (register_accessed) { + case GB_IO_IF: + case GB_IO_LCDC: + case GB_IO_STAT: + case GB_IO_SCY: + case GB_IO_SCX: + case GB_IO_LY: + case GB_IO_LYC: + case GB_IO_DMA: + case GB_IO_BGP: + case GB_IO_OBP0: + case GB_IO_OBP1: + case GB_IO_WY: + case GB_IO_WX: + case GB_IO_HDMA1: + case GB_IO_HDMA2: + case GB_IO_HDMA3: + case GB_IO_HDMA4: + case GB_IO_HDMA5: + case GB_IO_BGPI: + case GB_IO_BGPD: + case GB_IO_OBPI: + case GB_IO_OBPD: + case GB_IO_OPRI: + GB_display_sync(gb); + break; + } +} + static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) { @@ -246,8 +474,11 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } if (addr < 0xFF00) { + GB_display_sync(gb); if (gb->oam_write_blocked && !GB_is_cgb(gb)) { - GB_trigger_oam_bug_read(gb, addr); + if (!gb->disable_oam_corruption) { + GB_trigger_oam_bug_read(gb, addr); + } return 0xff; } @@ -257,28 +488,54 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } if (gb->oam_read_blocked) { - if (!GB_is_cgb(gb)) { + if (!GB_is_cgb(gb) && !gb->disable_oam_corruption) { 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; + + nodefault; + } for (unsigned i = 0; i < 8; i++) { gb->oam[(addr & 0xf8) + i] = gb->oam[0x98 + i]; @@ -302,24 +559,21 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) case GB_MODEL_AGB: return (addr & 0xF0) | ((addr >> 4) & 0xF); - /* case GB_MODEL_CGB_D: if (addr > 0xfec0) { addr |= 0xf0; } return gb->extra_oam[addr - 0xfea0]; - */ case GB_MODEL_CGB_C: - /* - case GB_MODEL_CGB_B: - case GB_MODEL_CGB_A: - case GB_MODEL_CGB_0: - */ + case GB_MODEL_CGB_B: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_0: addr &= ~0x18; return gb->extra_oam[addr - 0xfea0]; case GB_MODEL_DMG_B: + case GB_MODEL_MGB: case GB_MODEL_SGB_NTSC: case GB_MODEL_SGB_PAL: case GB_MODEL_SGB_NTSC_NO_SFC: @@ -331,12 +585,11 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } if (addr < 0xFF00) { - return 0; - } if (addr < 0xFF80) { + sync_ppu_if_needed(gb, addr); switch (addr & 0xFF) { case GB_IO_IF: return gb->io_registers[GB_IO_IF] | 0xE0; @@ -350,15 +603,18 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } return gb->io_registers[GB_IO_OPRI] | 0xFE; - case GB_IO_PCM_12: + case GB_IO_PCM12: if (!GB_is_cgb(gb)) return 0xFF; + GB_apu_run(gb, true); return ((gb->apu.is_active[GB_SQUARE_2] ? (gb->apu.samples[GB_SQUARE_2] << 4) : 0) | (gb->apu.is_active[GB_SQUARE_1] ? (gb->apu.samples[GB_SQUARE_1]) : 0)) & (gb->model <= GB_MODEL_CGB_C? gb->apu.pcm_mask[0] : 0xFF); - case GB_IO_PCM_34: + case GB_IO_PCM34: if (!GB_is_cgb(gb)) return 0xFF; + GB_apu_run(gb, true); return ((gb->apu.is_active[GB_NOISE] ? (gb->apu.samples[GB_NOISE] << 4) : 0) | (gb->apu.is_active[GB_WAVE] ? (gb->apu.samples[GB_WAVE]) : 0)) & (gb->model <= GB_MODEL_CGB_C? gb->apu.pcm_mask[1] : 0xFF); case GB_IO_JOYP: + gb->joyp_accessed = true; GB_timing_sync(gb); case GB_IO_TMA: case GB_IO_LCDC: @@ -416,7 +672,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) uint8_t index_reg = (addr & 0xFF) - 1; return ((addr & 0xFF) == GB_IO_BGPD? gb->background_palettes_data : - gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F]; + gb->object_palettes_data)[gb->io_registers[index_reg] & 0x3F]; } case GB_IO_KEY1: @@ -428,16 +684,19 @@ 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; } - case GB_IO_UNKNOWN2: - case GB_IO_UNKNOWN3: + case GB_IO_PSWX: + case GB_IO_PSWY: return GB_is_cgb(gb)? gb->io_registers[addr & 0xFF] : 0xFF; - case GB_IO_UNKNOWN4: + case GB_IO_PSW: return gb->cgb_mode? gb->io_registers[addr & 0xFF] : 0xFF; case GB_IO_UNKNOWN5: return GB_is_cgb(gb)? gb->io_registers[addr & 0xFF] | 0x8F : 0xFF; @@ -447,8 +706,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } return 0xFF; } - /* Hardware registers */ - return 0; + unreachable(); } if (addr == 0xFFFF) { @@ -460,7 +718,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) return gb->hram[addr - 0xFF80]; } -static GB_read_function_t * const read_map[] = +static read_function_t *const read_map[] = { read_rom, read_rom, read_rom, read_rom, /* 0XXX, 1XXX, 2XXX, 3XXX */ read_mbc_rom, read_mbc_rom, read_mbc_rom, read_mbc_rom, /* 4XXX, 5XXX, 6XXX, 7XXX */ @@ -477,15 +735,45 @@ void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t cal uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr) { - if (gb->n_watchpoints) { + if (unlikely(gb->n_watchpoints)) { GB_debugger_test_read_watchpoint(gb, addr); } - if (is_addr_in_dma_use(gb, addr)) { - addr = gb->dma_current_src; + if (unlikely(is_addr_in_dma_use(gb, addr))) { + if (GB_is_cgb(gb) && bus_for_addr(gb, addr) == GB_BUS_MAIN && gb->dma_current_src >= 0xe000) { + /* This is cart specific! Everdrive 7X on a CGB-A or 0 behaves differently. */ + return 0xFF; + } + + if (GB_is_cgb(gb) && bus_for_addr(gb, gb->dma_current_src) != GB_BUS_RAM && addr >= 0xc000) { + // TODO: this should probably affect the DMA dest as well + addr = (gb->dma_current_src & 0x1000) | (addr & 0xFFF) | 0xC000; + } + else if (GB_is_cgb(gb) && gb->dma_current_src >= 0xe000 && addr > 0xc000) { + // TODO: this should probably affect the DMA dest as well + addr = (gb->dma_current_src & 0x1000) | (addr & 0xFFF) | 0xC000; + } + else { + addr = gb->dma_current_src; + } } uint8_t data = read_map[addr >> 12](gb, addr); GB_apply_cheat(gb, addr, &data); - if (gb->read_memory_callback) { + if (unlikely(gb->read_memory_callback)) { + data = gb->read_memory_callback(gb, addr, data); + } + return data; +} + +uint8_t GB_safe_read_memory(GB_gameboy_t *gb, uint16_t addr) +{ + if (unlikely(addr == 0xFF00 + GB_IO_JOYP)) { + return gb->io_registers[GB_IO_JOYP]; + } + gb->disable_oam_corruption = true; + uint8_t data = read_map[addr >> 12](gb, addr); + gb->disable_oam_corruption = false; + GB_apply_cheat(gb, addr, &data); + if (unlikely(gb->read_memory_callback)) { data = gb->read_memory_callback(gb, addr, data); } return data; @@ -515,25 +803,22 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case 0x2000: case 0x3000: gb->mbc3.rom_bank = value; break; case 0x4000: case 0x5000: gb->mbc3.ram_bank = value; - gb->mbc3_rtc_mapped = value & 8; + 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; } @@ -542,6 +827,13 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) break; } break; + case GB_MBC7: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: gb->mbc_ram_enable = value == 0x0A; break; + case 0x2000: case 0x3000: gb->mbc7.rom_bank = value; break; + case 0x4000: case 0x5000: gb->mbc7.secondary_ram_enable = value == 0x40; break; + } + break; case GB_HUC1: switch (addr & 0xF000) { case 0x0000: case 0x1000: gb->huc1.ir_mode = (value & 0xF) == 0xE; break; @@ -553,94 +845,130 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_HUC3: switch (addr & 0xF000) { case 0x0000: case 0x1000: - gb->huc3_mode = value & 0xF; - gb->mbc_ram_enable = gb->huc3_mode == 0xA; + gb->huc3.mode = value & 0xF; + gb->mbc_ram_enable = gb->huc3.mode == 0xA; break; case 0x2000: case 0x3000: gb->huc3.rom_bank = value; break; 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; + nodefault; } GB_update_mbc_mappings(gb); } static void write_vram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { - if (gb->vram_write_blocked) { + GB_display_sync(gb); + if (unlikely(gb->vram_write_blocked)) { //GB_log(gb, "Wrote %02x to %04x (VRAM) during mode 3\n", value, addr); return; } - /* TODO: not verified */ - if (gb->display_state == 22 && GB_is_cgb(gb) && !gb->cgb_double_speed) { - if (addr & 0x1000) { - addr = gb->last_tile_index_address; - } - else if (gb->last_tile_data_address & 0x1000) { - /* TODO: This is case is more complicated then the rest and differ between revisions - It's probably affected by how VRAM is layed out, might be easier after a decap is done */ - } - else { - addr = gb->last_tile_data_address; - } - } - gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000] = value; + gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)] = value; } static bool huc3_write(GB_gameboy_t *gb, uint8_t value) { - switch (gb->huc3_mode) { + switch (gb->huc3.mode) { case 0xB: // RTC Write switch (value >> 4) { case 1: - if (gb->huc3_access_index < 3) { - gb->huc3_read = (gb->huc3_minutes >> (gb->huc3_access_index * 4)) & 0xF; + if (gb->huc3.access_index < 3) { + gb->huc3.read = (gb->huc3.minutes >> (gb->huc3.access_index * 4)) & 0xF; } - else if (gb->huc3_access_index < 7) { - gb->huc3_read = (gb->huc3_days >> ((gb->huc3_access_index - 3) * 4)) & 0xF; + else if (gb->huc3.access_index < 7) { + gb->huc3.read = (gb->huc3.days >> ((gb->huc3.access_index - 3) * 4)) & 0xF; } else { - // GB_log(gb, "Attempting to read from unsupported HuC-3 register: %03x\n", gb->huc3_access_index); + // GB_log(gb, "Attempting to read from unsupported HuC-3 register: %03x\n", gb->huc3.access_index); } - gb->huc3_access_index++; + gb->huc3.access_index++; break; case 2: case 3: - if (gb->huc3_access_index < 3) { - gb->huc3_minutes &= ~(0xF << (gb->huc3_access_index * 4)); - gb->huc3_minutes |= ((value & 0xF) << (gb->huc3_access_index * 4)); + if (gb->huc3.access_index < 3) { + gb->huc3.minutes &= ~(0xF << (gb->huc3.access_index * 4)); + gb->huc3.minutes |= ((value & 0xF) << (gb->huc3.access_index * 4)); } - else if (gb->huc3_access_index < 7) { - gb->huc3_days &= ~(0xF << ((gb->huc3_access_index - 3) * 4)); - gb->huc3_days |= ((value & 0xF) << ((gb->huc3_access_index - 3) * 4)); + else if (gb->huc3.access_index < 7) { + gb->huc3.days &= ~(0xF << ((gb->huc3.access_index - 3) * 4)); + gb->huc3.days |= ((value & 0xF) << ((gb->huc3.access_index - 3) * 4)); } - else if (gb->huc3_access_index >= 0x58 && gb->huc3_access_index <= 0x5a) { - gb->huc3_alarm_minutes &= ~(0xF << ((gb->huc3_access_index - 0x58) * 4)); - gb->huc3_alarm_minutes |= ((value & 0xF) << ((gb->huc3_access_index - 0x58) * 4)); + else if (gb->huc3.access_index >= 0x58 && gb->huc3.access_index <= 0x5a) { + gb->huc3.alarm_minutes &= ~(0xF << ((gb->huc3.access_index - 0x58) * 4)); + gb->huc3.alarm_minutes |= ((value & 0xF) << ((gb->huc3.access_index - 0x58) * 4)); } - else if (gb->huc3_access_index >= 0x5b && gb->huc3_access_index <= 0x5e) { - gb->huc3_alarm_days &= ~(0xF << ((gb->huc3_access_index - 0x5b) * 4)); - gb->huc3_alarm_days |= ((value & 0xF) << ((gb->huc3_access_index - 0x5b) * 4)); + else if (gb->huc3.access_index >= 0x5b && gb->huc3.access_index <= 0x5e) { + gb->huc3.alarm_days &= ~(0xF << ((gb->huc3.access_index - 0x5b) * 4)); + gb->huc3.alarm_days |= ((value & 0xF) << ((gb->huc3.access_index - 0x5b) * 4)); } - else if (gb->huc3_access_index == 0x5f) { - gb->huc3_alarm_enabled = value & 1; + else if (gb->huc3.access_index == 0x5f) { + gb->huc3.alarm_enabled = value & 1; } else { - // GB_log(gb, "Attempting to write %x to unsupported HuC-3 register: %03x\n", value & 0xF, gb->huc3_access_index); + // GB_log(gb, "Attempting to write %x to unsupported HuC-3 register: %03x\n", value & 0xF, gb->huc3.access_index); } if ((value >> 4) == 3) { - gb->huc3_access_index++; + gb->huc3.access_index++; } break; case 4: - gb->huc3_access_index &= 0xF0; - gb->huc3_access_index |= value & 0xF; + gb->huc3.access_index &= 0xF0; + gb->huc3.access_index |= value & 0xF; break; case 5: - gb->huc3_access_index &= 0x0F; - gb->huc3_access_index |= (value & 0xF) << 4; + gb->huc3.access_index &= 0x0F; + gb->huc3.access_index |= (value & 0xF) << 4; break; case 6: - gb->huc3_access_flags = (value & 0xF); + gb->huc3.access_flags = (value & 0xF); break; default: @@ -652,14 +980,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; } @@ -673,8 +998,133 @@ static bool huc3_write(GB_gameboy_t *gb, uint8_t value) } } +static void write_mbc7_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + if (!gb->mbc_ram_enable || !gb->mbc7.secondary_ram_enable) return; + if (addr >= 0xB000) return; + switch ((addr >> 4) & 0xF) { + case 0: { + if (value == 0x55) { + gb->mbc7.latch_ready = true; + gb->mbc7.x_latch = gb->mbc7.y_latch = 0x8000; + } + } + case 1: { + if (value == 0xAA) { + gb->mbc7.latch_ready = false; + gb->mbc7.x_latch = 0x81D0 + 0x70 * gb->accelerometer_x; + gb->mbc7.y_latch = 0x81D0 + 0x70 * gb->accelerometer_y; + } + } + case 8: { + gb->mbc7.eeprom_cs = value & 0x80; + gb->mbc7.eeprom_di = value & 2; + if (gb->mbc7.eeprom_cs) { + if (!gb->mbc7.eeprom_clk && (value & 0x40)) { // Clocked + gb->mbc7.eeprom_do = gb->mbc7.read_bits >> 15; + gb->mbc7.read_bits <<= 1; + gb->mbc7.read_bits |= 1; + if (gb->mbc7.bits_countdown == 0) { + /* Not transferring extra bits for a command*/ + gb->mbc7.eeprom_command <<= 1; + gb->mbc7.eeprom_command |= gb->mbc7.eeprom_di; + if (gb->mbc7.eeprom_command & 0x400) { + // Got full command + switch ((gb->mbc7.eeprom_command >> 6) & 0xF) { + case 0x8: + case 0x9: + case 0xA: + case 0xB: + // READ + gb->mbc7.read_bits = LE16(((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F]); + gb->mbc7.eeprom_command = 0; + break; + case 0x3: // EWEN (Eeprom Write ENable) + gb->mbc7.eeprom_write_enabled = true; + gb->mbc7.eeprom_command = 0; + break; + case 0x0: // EWDS (Eeprom Write DiSable) + gb->mbc7.eeprom_write_enabled = false; + gb->mbc7.eeprom_command = 0; + break; + case 0x4: + case 0x5: + case 0x6: + case 0x7: + // WRITE + if (gb->mbc7.eeprom_write_enabled) { + ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] = 0; + } + gb->mbc7.bits_countdown = 16; + // We still need to process this command, don't erase eeprom_command + break; + case 0xC: + case 0xD: + case 0xE: + case 0xF: + // ERASE + if (gb->mbc7.eeprom_write_enabled) { + ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] = 0xFFFF; + gb->mbc7.read_bits = 0x3FFF; // Emulate some time to settle + } + gb->mbc7.eeprom_command = 0; + break; + case 0x2: + // ERAL (ERase ALl) + if (gb->mbc7.eeprom_write_enabled) { + memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); + ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] = 0xFFFF; + gb->mbc7.read_bits = 0xFF; // Emulate some time to settle + } + gb->mbc7.eeprom_command = 0; + break; + case 0x1: + // WRAL (WRite ALl) + if (gb->mbc7.eeprom_write_enabled) { + memset(gb->mbc_ram, 0, gb->mbc_ram_size); + } + gb->mbc7.bits_countdown = 16; + // We still need to process this command, don't erase eeprom_command + break; + } + } + } + else { + // We're shifting in extra bits for a WRITE/WRAL command + gb->mbc7.bits_countdown--; + gb->mbc7.eeprom_do = true; + if (gb->mbc7.eeprom_di) { + uint16_t bit = LE16(1 << gb->mbc7.bits_countdown); + if (gb->mbc7.eeprom_command & 0x100) { + // WRITE + ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] |= bit; + } + else { + // WRAL + for (unsigned i = 0; i < 0x7F; i++) { + ((uint16_t *)gb->mbc_ram)[i] |= bit; + } + } + } + if (gb->mbc7.bits_countdown == 0) { // We're done + gb->mbc7.eeprom_command = 0; + gb->mbc7.read_bits = (gb->mbc7.eeprom_command & 0x100)? 0xFF : 0x3FFF; // Emulate some time to settle + } + } + } + } + gb->mbc7.eeprom_clk = value & 0x40; + } + } +} + static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { + if (gb->cartridge_type->mbc_type == GB_MBC7) { + write_mbc7_ram(gb, addr, value); + return; + } + if (gb->cartridge_type->mbc_type == GB_HUC3) { if (huc3_write(gb, value)) return; } @@ -684,24 +1134,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 +1175,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; } @@ -736,6 +1203,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } if (addr < 0xFF00) { + GB_display_sync(gb); if (gb->oam_write_blocked) { GB_trigger_oam_bug(gb, addr); return; @@ -749,35 +1217,34 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (GB_is_cgb(gb)) { if (addr < 0xFEA0) { gb->oam[addr & 0xFF] = value; + return; } switch (gb->model) { - /* case GB_MODEL_CGB_D: if (addr > 0xfec0) { addr |= 0xf0; } gb->extra_oam[addr - 0xfea0] = value; break; - */ case GB_MODEL_CGB_C: - /* - case GB_MODEL_CGB_B: - case GB_MODEL_CGB_A: - case GB_MODEL_CGB_0: - */ + case GB_MODEL_CGB_B: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_0: addr &= ~0x18; gb->extra_oam[addr - 0xfea0] = value; break; + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + break; case GB_MODEL_DMG_B: + case GB_MODEL_MGB: 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_E: - case GB_MODEL_AGB: - break; + unreachable(); } return; } @@ -817,6 +1284,8 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) /* Todo: Clean this code up: use a function table and move relevant code to display.c and timing.c (APU read and writes are already at apu.c) */ if (addr < 0xFF80) { + sync_ppu_if_needed(gb, addr); + /* Hardware registers */ switch (addr & 0xFF) { case GB_IO_WY: @@ -831,9 +1300,9 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_IO_OBP0: case GB_IO_OBP1: case GB_IO_SB: - case GB_IO_UNKNOWN2: - case GB_IO_UNKNOWN3: - case GB_IO_UNKNOWN4: + case GB_IO_PSWX: + case GB_IO_PSWY: + case GB_IO_PSW: case GB_IO_UNKNOWN5: gb->io_registers[addr & 0xFF] = value; return; @@ -901,17 +1370,35 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_IO_LCDC: if ((value & 0x80) && !(gb->io_registers[GB_IO_LCDC] & 0x80)) { + if (value & 0x80) { + // LCD turned on + if (!gb->lcd_disabled_outside_of_vblank && + (gb->cycles_since_vblank_callback > 10 * 456 || GB_is_sgb(gb))) { + // Trigger a vblank here so we don't exceed LCDC_PERIOD + GB_display_vblank(gb); + } + } + else { + // LCD turned off + if (gb->current_line < 144) { + // ROM might be repeatedly disabling LCDC outside of vblank, avoid callback spam + gb->lcd_disabled_outside_of_vblank = true; + } + } 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); } @@ -942,6 +1429,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; case GB_IO_DIV: + GB_set_internal_div_counter(gb, 0); /* Reset the div state machine */ gb->div_state = 0; gb->div_cycles = 0; @@ -978,6 +1466,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) read, it doesn't actually matter. */ gb->is_dma_restarting = true; } + gb->dma_and_pattern = 0xFF; gb->dma_cycles = -7; gb->dma_current_dest = 0; gb->dma_current_src = value << 8; @@ -985,12 +1474,11 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->io_registers[GB_IO_DMA] = value; return; case GB_IO_SVBK: - if (!gb->cgb_mode) { - return; - } - gb->cgb_ram_bank = value & 0x7; - if (!gb->cgb_ram_bank) { - gb->cgb_ram_bank++; + if (gb->cgb_mode || (GB_is_cgb(gb) && !gb->boot_rom_finished)) { + gb->cgb_ram_bank = value & 0x7; + if (!gb->cgb_ram_bank) { + gb->cgb_ram_bank++; + } } return; case GB_IO_VBK: @@ -1025,7 +1513,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } ((addr & 0xFF) == GB_IO_BGPD? gb->background_palettes_data : - gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F] = value; + gb->object_palettes_data)[gb->io_registers[index_reg] & 0x3F] = value; GB_palette_changed(gb, (addr & 0xFF) == GB_IO_BGPD, gb->io_registers[index_reg] & 0x3F); if (gb->io_registers[index_reg] & 0x80) { gb->io_registers[index_reg]++; @@ -1108,15 +1596,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; } @@ -1131,6 +1617,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } if (addr == 0xFFFF) { + GB_display_sync(gb); /* Interrupt mask */ gb->interrupt_enable = value; return; @@ -1142,42 +1629,86 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) -static GB_write_function_t * const write_map[] = +static write_function_t *const write_map[] = { write_mbc, write_mbc, write_mbc, write_mbc, /* 0XXX, 1XXX, 2XXX, 3XXX */ write_mbc, write_mbc, write_mbc, write_mbc, /* 4XXX, 5XXX, 6XXX, 7XXX */ write_vram, write_vram, /* 8XXX, 9XXX */ write_mbc_ram, write_mbc_ram, /* AXXX, BXXX */ write_ram, write_banked_ram, /* CXXX, DXXX */ - write_ram, write_high_memory, /* EXXX FXXX */ + write_ram, write_high_memory, /* EXXX FXXX */ }; +void GB_set_write_memory_callback(GB_gameboy_t *gb, GB_write_memory_callback_t callback) +{ + gb->write_memory_callback = callback; +} + void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { - if (gb->n_watchpoints) { + if (unlikely(gb->n_watchpoints)) { GB_debugger_test_write_watchpoint(gb, addr, value); } - if (is_addr_in_dma_use(gb, addr)) { - /* Todo: What should happen? Will this affect DMA? Will data be written? What and where? */ - return; + + if (unlikely(gb->write_memory_callback)) { + if (!gb->write_memory_callback(gb, addr, value)) return; + } + + if (unlikely(is_addr_in_dma_use(gb, addr))) { + bool oam_write = addr >= 0xFE00; + if (GB_is_cgb(gb) && bus_for_addr(gb, addr) == GB_BUS_MAIN && gb->dma_current_src >= 0xe000) { + /* This is cart specific! Everdrive 7X on a CGB-A or 0 behaves differently. */ + return; + } + + if (GB_is_cgb(gb) && bus_for_addr(gb, gb->dma_current_src) != GB_BUS_RAM && addr >= 0xc000) { + // TODO: this should probably affect the DMA dest as well + addr = (gb->dma_current_src & 0x1000) | (addr & 0xFFF) | 0xC000; + } + else if (GB_is_cgb(gb) && gb->dma_current_src >= 0xe000 && addr > 0xc000) { + // TODO: this should probably affect the DMA dest as well + addr = (gb->dma_current_src & 0x1000) | (addr & 0xFFF) | 0xC000; + } + else { + addr = gb->dma_current_src; + } + if (GB_is_cgb(gb) || addr > 0xc000) { + gb->dma_and_pattern = addr < 0xc000? 0x00 : 0xFF; + if ((gb->model < GB_MODEL_CGB_0 || gb->model == GB_MODEL_CGB_B) && addr > 0xc000) { + gb->dma_and_pattern = value; + } + else if ((gb->model < GB_MODEL_CGB_C || gb->model > GB_MODEL_CGB_E) && addr > 0xc000 && !oam_write) { + gb->dma_skip_write = true; + gb->oam[gb->dma_current_dest] = value; + } + if (gb->model < GB_MODEL_CGB_E || addr >= 0xc000) return; + } } write_map[addr >> 12](gb, addr, value); } void GB_dma_run(GB_gameboy_t *gb) { - while (gb->dma_cycles >= 4 && gb->dma_steps_left) { + while (unlikely(gb->dma_cycles >= 4 && gb->dma_steps_left)) { /* Todo: measure this value */ gb->dma_cycles -= 4; gb->dma_steps_left--; - - if (gb->dma_current_src < 0xe000) { - gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src); + if (gb->dma_skip_write) { + gb->dma_skip_write = false; + gb->dma_current_dest++; + } + else if (gb->dma_current_src < 0xe000) { + gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src) & gb->dma_and_pattern; } else { - /* Todo: Not correct on the CGB */ - gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src & ~0x2000); + if (GB_is_cgb(gb)) { + gb->oam[gb->dma_current_dest++] = gb->dma_and_pattern; + } + else { + gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src & ~0x2000) & gb->dma_and_pattern; + } } + gb->dma_and_pattern = 0xFF; /* dma_current_src must be the correct value during GB_read_memory */ gb->dma_current_src++; @@ -1189,7 +1720,7 @@ void GB_dma_run(GB_gameboy_t *gb) void GB_hdma_run(GB_gameboy_t *gb) { - if (!gb->hdma_on) return; + if (likely(!gb->hdma_on)) return; while (gb->hdma_cycles >= 0x4) { gb->hdma_cycles -= 0x4; diff --git a/Core/memory.h b/Core/memory.h index f0d0390..adfdcaa 100644 --- a/Core/memory.h +++ b/Core/memory.h @@ -1,18 +1,20 @@ #ifndef memory_h #define memory_h -#include "gb_struct_def.h" +#include "defs.h" #include typedef uint8_t (*GB_read_memory_callback_t)(GB_gameboy_t *gb, uint16_t addr, uint8_t data); +typedef bool (*GB_write_memory_callback_t)(GB_gameboy_t *gb, uint16_t addr, uint8_t data); // Return false to prevent the write void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t callback); +void GB_set_write_memory_callback(GB_gameboy_t *gb, GB_write_memory_callback_t callback); uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr); +uint8_t GB_safe_read_memory(GB_gameboy_t *gb, uint16_t addr); // Without side effects void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value); #ifdef GB_INTERNAL -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); +internal void GB_dma_run(GB_gameboy_t *gb); +internal void GB_hdma_run(GB_gameboy_t *gb); +internal void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address); #endif #endif /* memory_h */ diff --git a/Core/printer.c b/Core/printer.c index f04e54d..c8514b4 100644 --- a/Core/printer.c +++ b/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/Core/printer.h b/Core/printer.h index b29650f..f4ccfe4 100644 --- a/Core/printer.h +++ b/Core/printer.h @@ -2,7 +2,7 @@ #define printer_h #include #include -#include "gb_struct_def.h" +#include "defs.h" #define GB_PRINTER_MAX_COMMAND_LENGTH 0x280 #define GB_PRINTER_DATA_SIZE 0x280 @@ -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/Core/rewind.c b/Core/rewind.c index c3900d6..d305528 100644 --- a/Core/rewind.c +++ b/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/Core/rewind.h b/Core/rewind.h index ad54841..3cc23ed 100644 --- a/Core/rewind.h +++ b/Core/rewind.h @@ -2,11 +2,11 @@ #define rewind_h #include -#include "gb_struct_def.h" +#include "defs.h" #ifdef GB_INTERNAL -void GB_rewind_push(GB_gameboy_t *gb); -void GB_rewind_free(GB_gameboy_t *gb); +internal void GB_rewind_push(GB_gameboy_t *gb); +internal void GB_rewind_free(GB_gameboy_t *gb); #endif bool GB_rewind_pop(GB_gameboy_t *gb); void GB_set_rewind_length(GB_gameboy_t *gb, double seconds); diff --git a/Core/rumble.c b/Core/rumble.c index 8cbe20d..5f83c47 100644 --- a/Core/rumble.c +++ b/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/Core/rumble.h b/Core/rumble.h index eae9f37..ca34737 100644 --- a/Core/rumble.h +++ b/Core/rumble.h @@ -1,7 +1,7 @@ #ifndef rumble_h #define rumble_h -#include "gb_struct_def.h" +#include "defs.h" typedef enum { GB_RUMBLE_DISABLED, @@ -10,7 +10,7 @@ typedef enum { } GB_rumble_mode_t; #ifdef GB_INTERNAL -void GB_handle_rumble(GB_gameboy_t *gb); +internal void GB_handle_rumble(GB_gameboy_t *gb); #endif void GB_set_rumble_mode(GB_gameboy_t *gb, GB_rumble_mode_t mode); diff --git a/Core/save_state.c b/Core/save_state.c index 9ef6ae3..e090036 100644 --- a/Core/save_state.c +++ b/Core/save_state.c @@ -1,67 +1,247 @@ #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 + +_Static_assert((GB_SECTION_OFFSET(core_state) & 7) == 0, "Section core_state is not aligned"); +_Static_assert((GB_SECTION_OFFSET(dma) & 7) == 0, "Section dma is not aligned"); +_Static_assert((GB_SECTION_OFFSET(mbc) & 7) == 0, "Section mbc is not aligned"); +_Static_assert((GB_SECTION_OFFSET(hram) & 7) == 0, "Section hram is not aligned"); +_Static_assert((GB_SECTION_OFFSET(timing) & 7) == 0, "Section timing is not aligned"); +_Static_assert((GB_SECTION_OFFSET(apu) & 7) == 0, "Section apu is not aligned"); +_Static_assert((GB_SECTION_OFFSET(rtc) & 7) == 0, "Section rtc is not aligned"); +_Static_assert((GB_SECTION_OFFSET(video) & 7) == 0, "Section video is not aligned"); + +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 object_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,93 +258,33 @@ 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) +static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save, bool *attempt_bess) { - 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) -{ - 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); - } - if (save->ram_size == 0) { - /* Save doesn't have ram size specified, it's a pre 0.12 save state with potentially - incorrect RAM amount if it's a CGB instance */ - if (GB_is_cgb(save)) { - save->ram_size = 0x2000 * 8; // Incorrect RAM size - } - else { - save->ram_size = gb->ram_size; - } - } + *attempt_bess = false; if (gb->version != save->version) { GB_log(gb, "The save state is for a different version of SameBoy.\n"); + *attempt_bess = true; + return false; + } + + if (GB_is_cgb(gb) != GB_is_cgb(save) || GB_is_hle_sgb(gb) != GB_is_hle_sgb(save)) { + GB_log(gb, "The save state is for a different Game Boy model. Try changing the emulated model.\n"); return false; } @@ -184,17 +304,33 @@ static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t } if (gb->ram_size != save->ram_size) { - if (gb->ram_size == 0x1000 * 8 && save->ram_size == 0x2000 * 8) { - /* A bug in versions prior to 0.12 made CGB instances allocate twice the ammount of RAM. - Ignore this issue to retain compatibility with older, 0.11, save states. */ - } - else { - GB_log(gb, "The save state has non-matching RAM size. Try changing the emulated model.\n"); - return false; - } + GB_log(gb, "The save state has non-matching RAM size. Try changing the emulated model.\n"); + return false; } - return true; + switch (save->model) { + case GB_MODEL_DMG_B: return true; + case GB_MODEL_SGB_NTSC: return true; + case GB_MODEL_SGB_PAL: return true; + case GB_MODEL_SGB_NTSC_NO_SFC: return true; + case GB_MODEL_SGB_PAL_NO_SFC: return true; + case GB_MODEL_MGB: return true; + case GB_MODEL_SGB2: return true; + case GB_MODEL_SGB2_NO_SFC: return true; + case GB_MODEL_CGB_0: return true; + case GB_MODEL_CGB_A: return true; + case GB_MODEL_CGB_B: return true; + case GB_MODEL_CGB_C: return true; + case GB_MODEL_CGB_D: return true; + case GB_MODEL_CGB_E: return true; + case GB_MODEL_AGB: return true; + } + if ((gb->model & GB_MODEL_FAMILY_MASK) == (save->model & GB_MODEL_FAMILY_MASK)) { + save->model = gb->model; + return true; + } + GB_log(gb, "This save state is for an unknown Game Boy model\n"); + return false; } static void sanitize_state(GB_gameboy_t *gb) @@ -208,20 +344,819 @@ static void sanitize_state(GB_gameboy_t *gb) gb->bg_fifo.write_end &= 0xF; gb->oam_fifo.read_end &= 0xF; gb->oam_fifo.write_end &= 0xF; + gb->last_tile_index_address &= 0x1FFF; + gb->window_tile_x &= 0x1F; + + /* These are kind of DOS-ish if too large */ + if (abs(gb->display_cycles) > 0x80000) { + gb->display_cycles = 0; + } + + if (abs(gb->div_cycles) > 0x8000) { + gb->div_cycles = 0; + } + + if (!GB_is_cgb(gb)) { + gb->cgb_mode = false; + } + + if (gb->ram_size == 0x8000) { + gb->cgb_ram_bank &= 0x7; + } + else { + gb->cgb_ram_bank = 1; + } + if (gb->vram_size != 0x4000) { + gb->cgb_vram_bank = 0; + } + if (!GB_is_cgb(gb)) { + gb->current_tile_attributes = 0; + } + + if ((unsigned)gb->dma_current_dest + (unsigned)gb->dma_steps_left >= 0xa0) { + gb->dma_current_dest = 0; + gb->dma_steps_left = 0; + } + gb->object_low_line_address &= gb->vram_size & ~1; - gb->fetcher_x &= 0x1f; if (gb->lcd_x > gb->position_in_line) { gb->lcd_x = gb->position_in_line; } - 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) { + if (gb->sgb->player_count != 1 && gb->sgb->player_count != 2 && gb->sgb->player_count != 4) { + gb->sgb->player_count = 1; + } + gb->sgb->current_player &= gb->sgb->player_count - 1; + } + GB_update_clock_rate(gb); +} + +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; +} + +#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)) { + 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_MGB: bess_core.full_model = BE32('GM '); 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_0: bess_core.full_model = BE32('CC0 '); break; + case GB_MODEL_CGB_A: bess_core.full_model = BE32('CCA '); break; + case GB_MODEL_CGB_B: bess_core.full_model = BE32('CCB '); break; + case GB_MODEL_CGB_C: bess_core.full_model = BE32('CCC '); break; + case GB_MODEL_CGB_D: bess_core.full_model = BE32('CCD '); 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.object_palettes.size = LE32(sizeof(gb->object_palettes_data)); + bess_core.object_palettes.offset = LE32(video_offset + offsetof(GB_gameboy_t, object_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; + 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)); } } -#define READ_SECTION(gb, f, section) read_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section), fix_broken_windows_saves) +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; + } -int GB_load_state(GB_gameboy_t *gb, const char *path) + + 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; + + // Done early for compatibility with 0.14.x + GB_write_memory(&save, 0xFF00 + GB_IO_SVBK, core.io_registers[GB_IO_SVBK]); + // 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]); + + // 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; + if (LE32(block.size) > 0x1000) 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.object_palettes, file, gb->object_palettes_data, sizeof(gb->object_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); + sanitize_state(gb); + return errno; +} + +static int load_state_internal(GB_gameboy_t *gb, virtual_file_t *file) { GB_gameboy_t save; @@ -230,184 +1165,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_save_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/Core/save_state.h b/Core/save_state.h index fcb9135..2352cac 100644 --- a/Core/save_state.h +++ b/Core/save_state.h @@ -5,10 +5,16 @@ #define GB_PADDING(type, old_usage) type old_usage##__do_not_use -#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) union {uint8_t name##_section_start; struct {__VA_ARGS__};}; uint8_t name##_section_end[0] +#ifdef __cplusplus +/* For bsnes integration. C++ code does not need section information, and throws a fit over certain types such + as anonymous enums inside unions */ +#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) __VA_ARGS__ +#else +#define GB_SECTION(name, ...) union __attribute__ ((aligned (8))) {uint8_t name##_section_start; struct {__VA_ARGS__};}; uint8_t name##_section_end[0] #define GB_SECTION_OFFSET(name) (offsetof(GB_gameboy_t, name##_section_start)) #define GB_SECTION_SIZE(name) (offsetof(GB_gameboy_t, name##_section_end) - offsetof(GB_gameboy_t, name##_section_start)) #define GB_GET_SECTION(gb, name) ((void*)&((gb)->name##_section_start)) +#endif #define GB_aligned_double __attribute__ ((aligned (8))) double @@ -21,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_save_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 */ +internal size_t GB_get_save_state_size_no_bess(GB_gameboy_t *gb); +internal void GB_save_state_to_buffer_no_bess(GB_gameboy_t *gb, uint8_t *buffer); +#endif + #endif /* save_state_h */ diff --git a/Core/sgb.c b/Core/sgb.c index c77b0db..7cdd77f 100644 --- a/Core/sgb.c +++ b/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; } } @@ -264,12 +262,10 @@ static void command_ready(GB_gameboy_t *gb) } *command = (void *)(gb->sgb->command + 1); uint16_t count = command->length; -#ifdef GB_BIG_ENDIAN - count = __builtin_bswap16(count); -#endif + count = LE16(count); 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 +279,7 @@ static void command_ready(GB_gameboy_t *gb) x++; y = 0; if (x == 20) { - x = 0; + break; } } } @@ -293,7 +289,7 @@ static void command_ready(GB_gameboy_t *gb) y++; x = 0; if (y == 18) { - y = 0; + break; } } } @@ -377,39 +373,36 @@ static void command_ready(GB_gameboy_t *gb) } break; case PAL_TRN: - gb->sgb->vram_transfer_countdown = 2; + gb->sgb->vram_transfer_countdown = 3; gb->sgb->transfer_dest = TRANSFER_PALETTES; break; case DATA_SND: // Not supported, but used by almost all SGB games for hot patching, so let's mute the warning for this break; case MLT_REQ: - if (gb->sgb->player_count == 1) { - gb->sgb->current_player = 0; - } gb->sgb->player_count = (gb->sgb->command[1] & 3) + 1; /* Todo: When breaking save state comaptibility, fix this to be 0 based. */ if (gb->sgb->player_count == 3) { - gb->sgb->current_player++; + gb->sgb->player_count++; } - gb->sgb->mlt_lock = true; + gb->sgb->current_player &= (gb->sgb->player_count - 1); break; case CHR_TRN: - gb->sgb->vram_transfer_countdown = 2; + gb->sgb->vram_transfer_countdown = 3; gb->sgb->transfer_dest = (gb->sgb->command[1] & 1)? TRANSFER_HIGH_TILES : TRANSFER_LOW_TILES; break; case PCT_TRN: - gb->sgb->vram_transfer_countdown = 2; + gb->sgb->vram_transfer_countdown = 3; gb->sgb->transfer_dest = TRANSFER_BORDER_DATA; break; case ATTR_TRN: - gb->sgb->vram_transfer_countdown = 2; + gb->sgb->vram_transfer_countdown = 3; gb->sgb->transfer_dest = TRANSFER_ATTRIBUTES; break; case ATTR_SET: - load_attribute_file(gb, gb->sgb->command[0] & 0x3F); + load_attribute_file(gb, gb->sgb->command[1] & 0x3F); - if (gb->sgb->command[0] & 0x40) { + if (gb->sgb->command[1] & 0x40) { gb->sgb->mask_mode = MASK_DISABLED; } break; @@ -439,27 +432,22 @@ void GB_sgb_write(GB_gameboy_t *gb, uint8_t value) return; } if (gb->sgb->disable_commands) return; - if (gb->sgb->command_write_index >= sizeof(gb->sgb->command) * 8) { - return; - } uint16_t command_size = (gb->sgb->command[0] & 7 ?: 1) * SGB_PACKET_SIZE * 8; if ((gb->sgb->command[0] & 0xF1) == 0xF1) { command_size = SGB_PACKET_SIZE * 8; } - if ((value & 0x20) == 0 && (gb->io_registers[GB_IO_JOYP] & 0x20) != 0) { - gb->sgb->mlt_lock ^= true; + if ((value & 0x20) != 0 && (gb->io_registers[GB_IO_JOYP] & 0x20) == 0) { + if ((gb->sgb->player_count & 1) == 0) { + gb->sgb->current_player++; + gb->sgb->current_player &= (gb->sgb->player_count - 1); + } } switch ((value >> 4) & 3) { case 3: gb->sgb->ready_for_pulse = true; - if ((gb->sgb->player_count & 1) == 0 && !gb->sgb->mlt_lock) { - gb->sgb->current_player++; - gb->sgb->current_player &= 3; - gb->sgb->mlt_lock = true; - } break; case 2: // Zero @@ -475,10 +463,12 @@ void GB_sgb_write(GB_gameboy_t *gb, uint8_t value) gb->sgb->ready_for_stop = false; } else { - gb->sgb->command_write_index++; - gb->sgb->ready_for_pulse = false; - if (((gb->sgb->command_write_index) & (SGB_PACKET_SIZE * 8 - 1)) == 0) { - gb->sgb->ready_for_stop = true; + if (gb->sgb->command_write_index < sizeof(gb->sgb->command) * 8) { + gb->sgb->command_write_index++; + gb->sgb->ready_for_pulse = false; + if (((gb->sgb->command_write_index) & (SGB_PACKET_SIZE * 8 - 1)) == 0) { + gb->sgb->ready_for_stop = true; + } } } break; @@ -492,11 +482,13 @@ void GB_sgb_write(GB_gameboy_t *gb, uint8_t value) memset(gb->sgb->command, 0, sizeof(gb->sgb->command)); } else { - gb->sgb->command[gb->sgb->command_write_index / 8] |= 1 << (gb->sgb->command_write_index & 7); - gb->sgb->command_write_index++; - gb->sgb->ready_for_pulse = false; - if (((gb->sgb->command_write_index) & (SGB_PACKET_SIZE * 8 - 1)) == 0) { - gb->sgb->ready_for_stop = true; + if (gb->sgb->command_write_index < sizeof(gb->sgb->command) * 8) { + gb->sgb->command[gb->sgb->command_write_index / 8] |= 1 << (gb->sgb->command_write_index & 7); + gb->sgb->command_write_index++; + gb->sgb->ready_for_pulse = false; + if (((gb->sgb->command_write_index) & (SGB_PACKET_SIZE * 8 - 1)) == 0) { + gb->sgb->ready_for_stop = true; + } } } break; @@ -539,7 +531,6 @@ static uint32_t convert_rgb15_with_fade(GB_gameboy_t *gb, uint16_t color, uint8_ return GB_convert_rgb15(gb, color, false); } -#include static void render_boot_animation (GB_gameboy_t *gb) { #include "graphics/sgb_animation_logo.inc" @@ -556,8 +547,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 +598,73 @@ 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; } + *data = LE16(*data); + 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 = 105; // Measured on an SGB2, but might be off by ±2 } } } - 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 +673,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 +725,24 @@ 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->border_animation > 64 || gb->sgb->intro_animation < GB_SGB_INTRO_ANIMATION_LENGTH) { + if (gb->sgb->border_animation != 0) { + gb->sgb->border_animation--; + } 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 +760,20 @@ 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]); + if (tile & 0x300) continue; // Unused tile + 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 +798,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 +822,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 +871,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 +889,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/Core/sgb.h b/Core/sgb.h index aae9f75..4ed2bea 100644 --- a/Core/sgb.h +++ b/Core/sgb.h @@ -1,12 +1,12 @@ #ifndef sgb_h #define sgb_h -#include "gb_struct_def.h" +#include "defs.h" #include #include typedef struct GB_sgb_s GB_sgb_t; typedef struct { - uint8_t tiles[0x100 * 8 * 8]; /* High nibble not used*/ + uint8_t tiles[0x100 * 8 * 4]; union { struct { uint16_t map[32 * 32]; @@ -17,6 +17,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,21 +48,19 @@ 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; /* GB Header */ uint8_t received_header[0x54]; - - /* Multiplayer (cont) */ - bool mlt_lock; }; -void GB_sgb_write(GB_gameboy_t *gb, uint8_t value); -void GB_sgb_render(GB_gameboy_t *gb); -void GB_sgb_load_default_data(GB_gameboy_t *gb); +internal void GB_sgb_write(GB_gameboy_t *gb, uint8_t value); +internal void GB_sgb_render(GB_gameboy_t *gb); +internal void GB_sgb_load_default_data(GB_gameboy_t *gb); #endif diff --git a/Core/sm83_cpu.c b/Core/sm83_cpu.c index 3b3eceb..fdaf85d 100644 --- a/Core/sm83_cpu.c +++ b/Core/sm83_cpu.c @@ -4,7 +4,7 @@ #include "gb.h" -typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode); +typedef void opcode_t(GB_gameboy_t *gb, uint8_t opcode); typedef enum { /* Default behavior. If the CPU writes while another component reads, it reads the old value */ @@ -21,22 +21,27 @@ typedef enum { GB_CONFLICT_DMG_LCDC, GB_CONFLICT_SGB_LCDC, GB_CONFLICT_WX, -} GB_conflict_t; + GB_CONFLICT_CGB_LCDC, + GB_CONFLICT_NR10, +} conflict_t; /* Todo: How does double speed mode affect these? */ -static const GB_conflict_t cgb_conflict_map[0x80] = { +static const 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, + [GB_IO_SCX] = GB_CONFLICT_WRITE_CPU, // TODO: Similar to BGP, there's some time travelling involved + /* Todo: most values not verified, and probably differ between revisions */ }; /* Todo: verify on an MGB */ -static const GB_conflict_t dmg_conflict_map[0x80] = { +static const conflict_t dmg_conflict_map[0x80] = { [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, [GB_IO_LYC] = GB_CONFLICT_READ_OLD, [GB_IO_LCDC] = GB_CONFLICT_DMG_LCDC, @@ -48,13 +53,14 @@ 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, }; /* Todo: Verify on an SGB1 */ -static const GB_conflict_t sgb_conflict_map[0x80] = { +static const conflict_t sgb_conflict_map[0x80] = { [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, [GB_IO_LYC] = GB_CONFLICT_READ_OLD, [GB_IO_LCDC] = GB_CONFLICT_SGB_LCDC, @@ -66,7 +72,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, }; @@ -76,17 +83,7 @@ static uint8_t cycle_read(GB_gameboy_t *gb, uint16_t addr) if (gb->pending_cycles) { GB_advance_cycles(gb, gb->pending_cycles); } - uint8_t ret = GB_read_memory(gb, addr); - gb->pending_cycles = 4; - 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 */ + gb->address_bus = addr; uint8_t ret = GB_read_memory(gb, addr); gb->pending_cycles = 4; return ret; @@ -97,10 +94,12 @@ static uint8_t cycle_read_inc_oam_bug(GB_gameboy_t *gb, uint16_t addr) is both read be the CPU, modified by the ISR, and modified by an actual interrupt. If this timing proves incorrect, the ISR emulation must be updated so IF reads are timed correctly. */ +/* TODO: Does this affect the address bus? Verify. */ static uint8_t cycle_write_if(GB_gameboy_t *gb, uint8_t value) { assert(gb->pending_cycles); GB_advance_cycles(gb, gb->pending_cycles); + gb->address_bus = 0xFF00 + GB_IO_IF; uint8_t old = (gb->io_registers[GB_IO_IF]) & 0x1F; GB_write_memory(gb, 0xFF00 + GB_IO_IF, value); gb->pending_cycles = 4; @@ -110,9 +109,9 @@ static uint8_t cycle_write_if(GB_gameboy_t *gb, uint8_t value) static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { assert(gb->pending_cycles); - GB_conflict_t conflict = GB_CONFLICT_READ_OLD; + conflict_t conflict = GB_CONFLICT_READ_OLD; if ((addr & 0xFF80) == 0xFF00) { - const GB_conflict_t *map = NULL; + const conflict_t *map = NULL; if (GB_is_cgb(gb)) { map = cgb_conflict_map; } @@ -129,19 +128,19 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_advance_cycles(gb, gb->pending_cycles); GB_write_memory(gb, addr, value); gb->pending_cycles = 4; - return; + break; case GB_CONFLICT_READ_NEW: GB_advance_cycles(gb, gb->pending_cycles - 1); GB_write_memory(gb, addr, value); gb->pending_cycles = 5; - return; + break; case GB_CONFLICT_WRITE_CPU: GB_advance_cycles(gb, gb->pending_cycles + 1); GB_write_memory(gb, addr, value); gb->pending_cycles = 3; - return; + break; /* The DMG STAT-write bug is basically the STAT register being read as FF for a single T-cycle */ case GB_CONFLICT_STAT_DMG: @@ -159,7 +158,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_advance_cycles(gb, 1); GB_write_memory(gb, addr, value); gb->pending_cycles = 3; - return; + break; case GB_CONFLICT_STAT_CGB: { /* Todo: Verify this with SCX adjustments */ @@ -170,7 +169,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_advance_cycles(gb, 1); GB_write_memory(gb, addr, value); gb->pending_cycles = 3; - return; + break; } /* There is some "time travel" going on with these two values, as it appears @@ -185,21 +184,21 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_advance_cycles(gb, 1); GB_write_memory(gb, addr, value); gb->pending_cycles = 5; - return; + break; } case GB_CONFLICT_PALETTE_CGB: { GB_advance_cycles(gb, gb->pending_cycles - 2); GB_write_memory(gb, addr, value); gb->pending_cycles = 6; - return; + break; } case GB_CONFLICT_DMG_LCDC: { /* Similar to the palette registers, these interact directly with the LCD, so they appear to be affected by it. Both my DMG (B, blob) and Game Boy Light behave this way though. Additionally, LCDC.1 is very nasty because on the it is read both by the FIFO when popping pixels, - and the sprite-fetching state machine, and both behave differently when it comes to access conflicts. + and the object-fetching state machine, and both behave differently when it comes to access conflicts. Hacks ahead. */ @@ -208,7 +207,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) uint8_t old_value = GB_read_memory(gb, addr); GB_advance_cycles(gb, gb->pending_cycles - 2); - if (/* gb->model != GB_MODEL_MGB && */ gb->position_in_line == 0 && (old_value & 2) && !(value & 2)) { + if (gb->model != GB_MODEL_MGB && gb->position_in_line == 0 && (old_value & 2) && !(value & 2)) { old_value &= ~2; } @@ -216,7 +215,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_advance_cycles(gb, 1); GB_write_memory(gb, addr, value); gb->pending_cycles = 5; - return; + break; } case GB_CONFLICT_SGB_LCDC: { @@ -230,7 +229,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_advance_cycles(gb, 1); GB_write_memory(gb, addr, value); gb->pending_cycles = 5; - return; + break; } case GB_CONFLICT_WX: @@ -240,8 +239,58 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_advance_cycles(gb, 1); gb->wx_just_changed = false; gb->pending_cycles = 3; - return; + break; + + 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; + } + break; + + 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; + break; } + gb->address_bus = addr; } static void cycle_no_access(GB_gameboy_t *gb) @@ -251,17 +300,22 @@ static void cycle_no_access(GB_gameboy_t *gb) static void cycle_oam_bug(GB_gameboy_t *gb, uint8_t register_id) { - if (GB_is_cgb(gb)) { - /* Slight optimization */ - gb->pending_cycles += 4; - return; - } if (gb->pending_cycles) { GB_advance_cycles(gb, gb->pending_cycles); } + gb->address_bus = gb->registers[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->pending_cycles) { + GB_advance_cycles(gb, gb->pending_cycles); + } + gb->address_bus = gb->pc; + 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 +341,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 +353,74 @@ 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; - - enter_stop_mode(gb); - leave_stop_mode(gb); - - if (!needs_alignment) { - GB_advance_cycles(gb, 0x4); - } - + flush_pending_cycles(gb); + if ((gb->io_registers[GB_IO_JOYP] & 0x30) != 0x30) { + gb->joyp_accessed = true; } - 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); - } + 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); } - /* 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 +439,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; } @@ -371,7 +448,7 @@ static void ld_drr_a(GB_gameboy_t *gb, uint8_t opcode) { uint8_t register_id; register_id = (opcode >> 4) + 1; - cycle_write(gb, gb->registers[register_id], gb->registers[GB_REGISTER_AF] >> 8); + cycle_write(gb, gb->registers[register_id], gb->af >> 8); } static void inc_rr(GB_gameboy_t *gb, uint8_t opcode) @@ -386,14 +463,14 @@ static void inc_hr(GB_gameboy_t *gb, uint8_t opcode) uint8_t register_id; register_id = ((opcode >> 4) + 1) & 0x03; gb->registers[register_id] += 0x100; - gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->af &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); if ((gb->registers[register_id] & 0x0F00) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ((gb->registers[register_id] & 0xFF00) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } static void dec_hr(GB_gameboy_t *gb, uint8_t opcode) @@ -401,15 +478,15 @@ static void dec_hr(GB_gameboy_t *gb, uint8_t opcode) uint8_t register_id; register_id = ((opcode >> 4) + 1) & 0x03; gb->registers[register_id] -= 0x100; - gb->registers[GB_REGISTER_AF] &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); - gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + gb->af &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->af |= GB_SUBTRACT_FLAG; if ((gb->registers[register_id] & 0x0F00) == 0xF00) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ((gb->registers[register_id] & 0xFF00) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -418,30 +495,30 @@ 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) { - bool carry = (gb->registers[GB_REGISTER_AF] & 0x8000) != 0; + bool carry = (gb->af & 0x8000) != 0; - gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] & 0xFF00) << 1; + gb->af = (gb->af & 0xFF00) << 1; if (carry) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG | 0x0100; + gb->af |= GB_CARRY_FLAG | 0x0100; } } static void rla(GB_gameboy_t *gb, uint8_t opcode) { - bool bit7 = (gb->registers[GB_REGISTER_AF] & 0x8000) != 0; - bool carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + bool bit7 = (gb->af & 0x8000) != 0; + bool carry = (gb->af & GB_CARRY_FLAG) != 0; - gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] & 0xFF00) << 1; + gb->af = (gb->af & 0xFF00) << 1; if (carry) { - gb->registers[GB_REGISTER_AF] |= 0x0100; + gb->af |= 0x0100; } if (bit7) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -449,30 +526,30 @@ 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; - cycle_write(gb, addr, gb->registers[GB_REGISTER_SP] & 0xFF); - cycle_write(gb, addr + 1, gb->registers[GB_REGISTER_SP] >> 8); + addr = cycle_read(gb, gb->pc++); + addr |= cycle_read(gb, gb->pc++) << 8; + cycle_write(gb, addr, gb->sp & 0xFF); + cycle_write(gb, addr + 1, gb->sp >> 8); } static void add_hl_rr(GB_gameboy_t *gb, uint8_t opcode) { - uint16_t hl = gb->registers[GB_REGISTER_HL]; + uint16_t hl = gb->hl; uint16_t rr; uint8_t register_id; cycle_no_access(gb); register_id = (opcode >> 4) + 1; rr = gb->registers[register_id]; - gb->registers[GB_REGISTER_HL] = hl + rr; - gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_CARRY_FLAG | GB_HALF_CARRY_FLAG); + gb->hl = hl + rr; + gb->af &= ~(GB_SUBTRACT_FLAG | GB_CARRY_FLAG | GB_HALF_CARRY_FLAG); /* The meaning of the Half Carry flag is really hard to track -_- */ if (((hl & 0xFFF) + (rr & 0xFFF)) & 0x1000) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ( ((unsigned) hl + (unsigned) rr) & 0x10000) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -480,8 +557,8 @@ static void ld_a_drr(GB_gameboy_t *gb, uint8_t opcode) { uint8_t register_id; register_id = (opcode >> 4) + 1; - gb->registers[GB_REGISTER_AF] &= 0xFF; - gb->registers[GB_REGISTER_AF] |= cycle_read(gb, gb->registers[register_id]) << 8; + gb->af &= 0xFF; + gb->af |= cycle_read(gb, gb->registers[register_id]) << 8; } static void dec_rr(GB_gameboy_t *gb, uint8_t opcode) @@ -500,14 +577,14 @@ static void inc_lr(GB_gameboy_t *gb, uint8_t opcode) value = (gb->registers[register_id] & 0xFF) + 1; gb->registers[register_id] = (gb->registers[register_id] & 0xFF00) | value; - gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->af &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); if ((gb->registers[register_id] & 0x0F) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ((gb->registers[register_id] & 0xFF) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } static void dec_lr(GB_gameboy_t *gb, uint8_t opcode) @@ -519,15 +596,15 @@ static void dec_lr(GB_gameboy_t *gb, uint8_t opcode) value = (gb->registers[register_id] & 0xFF) - 1; gb->registers[register_id] = (gb->registers[register_id] & 0xFF00) | value; - gb->registers[GB_REGISTER_AF] &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); - gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + gb->af &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->af |= GB_SUBTRACT_FLAG; if ((gb->registers[register_id] & 0x0F) == 0xF) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ((gb->registers[register_id] & 0xFF) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -536,37 +613,37 @@ 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) { - bool carry = (gb->registers[GB_REGISTER_AF] & 0x100) != 0; + bool carry = (gb->af & 0x100) != 0; - gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] >> 1) & 0xFF00; + gb->af = (gb->af >> 1) & 0xFF00; if (carry) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG | 0x8000; + gb->af |= GB_CARRY_FLAG | 0x8000; } } static void rra(GB_gameboy_t *gb, uint8_t opcode) { - bool bit1 = (gb->registers[GB_REGISTER_AF] & 0x0100) != 0; - bool carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + bool bit1 = (gb->af & 0x0100) != 0; + bool carry = (gb->af & GB_CARRY_FLAG) != 0; - gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] >> 1) & 0xFF00; + gb->af = (gb->af >> 1) & 0xFF00; if (carry) { - gb->registers[GB_REGISTER_AF] |= 0x8000; + gb->af |= 0x8000; } if (bit1) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } 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); } @@ -574,13 +651,14 @@ static bool condition_code(GB_gameboy_t *gb, uint8_t opcode) { switch ((opcode >> 3) & 0x3) { case 0: - return !(gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + return !(gb->af & GB_ZERO_FLAG); case 1: - return (gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + return (gb->af & GB_ZERO_FLAG); case 2: - return !(gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + return !(gb->af & GB_CARRY_FLAG); case 3: - return (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + return (gb->af & GB_CARRY_FLAG); + nodefault; } return false; @@ -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); @@ -597,118 +675,118 @@ static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode) static void daa(GB_gameboy_t *gb, uint8_t opcode) { - int16_t result = gb->registers[GB_REGISTER_AF] >> 8; + int16_t result = gb->af >> 8; - gb->registers[GB_REGISTER_AF] &= ~(0xFF00 | GB_ZERO_FLAG); + gb->af &= ~(0xFF00 | GB_ZERO_FLAG); - if (gb->registers[GB_REGISTER_AF] & GB_SUBTRACT_FLAG) { - if (gb->registers[GB_REGISTER_AF] & GB_HALF_CARRY_FLAG) { + if (gb->af & GB_SUBTRACT_FLAG) { + if (gb->af & GB_HALF_CARRY_FLAG) { result = (result - 0x06) & 0xFF; } - if (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) { + if (gb->af & GB_CARRY_FLAG) { result -= 0x60; } } else { - if ((gb->registers[GB_REGISTER_AF] & GB_HALF_CARRY_FLAG) || (result & 0x0F) > 0x09) { + if ((gb->af & GB_HALF_CARRY_FLAG) || (result & 0x0F) > 0x09) { result += 0x06; } - if ((gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) || result > 0x9F) { + if ((gb->af & GB_CARRY_FLAG) || result > 0x9F) { result += 0x60; } } if ((result & 0xFF) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((result & 0x100) == 0x100) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } - gb->registers[GB_REGISTER_AF] &= ~GB_HALF_CARRY_FLAG; - gb->registers[GB_REGISTER_AF] |= result << 8; + gb->af &= ~GB_HALF_CARRY_FLAG; + gb->af |= result << 8; } static void cpl(GB_gameboy_t *gb, uint8_t opcode) { - gb->registers[GB_REGISTER_AF] ^= 0xFF00; - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG; + gb->af ^= 0xFF00; + gb->af |= GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG; } static void scf(GB_gameboy_t *gb, uint8_t opcode) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; - gb->registers[GB_REGISTER_AF] &= ~(GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG); + gb->af |= GB_CARRY_FLAG; + gb->af &= ~(GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG); } static void ccf(GB_gameboy_t *gb, uint8_t opcode) { - gb->registers[GB_REGISTER_AF] ^= GB_CARRY_FLAG; - gb->registers[GB_REGISTER_AF] &= ~(GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG); + gb->af ^= GB_CARRY_FLAG; + gb->af &= ~(GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG); } static void ld_dhli_a(GB_gameboy_t *gb, uint8_t opcode) { - cycle_write(gb, gb->registers[GB_REGISTER_HL]++, gb->registers[GB_REGISTER_AF] >> 8); + cycle_write(gb, gb->hl++, gb->af >> 8); } static void ld_dhld_a(GB_gameboy_t *gb, uint8_t opcode) { - cycle_write(gb, gb->registers[GB_REGISTER_HL]--, gb->registers[GB_REGISTER_AF] >> 8); + cycle_write(gb, gb->hl--, gb->af >> 8); } 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->af &= 0xFF; + gb->af |= cycle_read(gb, gb->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->af &= 0xFF; + gb->af |= cycle_read(gb, gb->hl--) << 8; } static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value; - value = cycle_read(gb, gb->registers[GB_REGISTER_HL]) + 1; - cycle_write(gb, gb->registers[GB_REGISTER_HL], value); + value = cycle_read(gb, gb->hl) + 1; + cycle_write(gb, gb->hl, value); - gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->af &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); if ((value & 0x0F) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ((value & 0xFF) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } static void dec_dhl(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value; - value = cycle_read(gb, gb->registers[GB_REGISTER_HL]) - 1; - cycle_write(gb, gb->registers[GB_REGISTER_HL], value); + value = cycle_read(gb, gb->hl) - 1; + cycle_write(gb, gb->hl, value); - gb->registers[GB_REGISTER_AF] &= ~( GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); - gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + gb->af &= ~( GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->af |= GB_SUBTRACT_FLAG; if ((value & 0x0F) == 0x0F) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ((value & 0xFF) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode) { - uint8_t data = cycle_read_inc_oam_bug(gb, gb->pc++); - cycle_write(gb, gb->registers[GB_REGISTER_HL], data); + uint8_t data = cycle_read(gb, gb->pc++); + cycle_write(gb, gb->hl, data); } static uint8_t get_src_value(GB_gameboy_t *gb, uint8_t opcode) @@ -719,9 +797,9 @@ static uint8_t get_src_value(GB_gameboy_t *gb, uint8_t opcode) src_low = opcode & 1; if (src_register_id == GB_REGISTER_AF) { if (src_low) { - return gb->registers[GB_REGISTER_AF] >> 8; + return gb->af >> 8; } - return cycle_read(gb, gb->registers[GB_REGISTER_HL]); + return cycle_read(gb, gb->hl); } if (src_low) { return gb->registers[src_register_id] & 0xFF; @@ -738,11 +816,11 @@ static void set_src_value(GB_gameboy_t *gb, uint8_t opcode, uint8_t value) if (src_register_id == GB_REGISTER_AF) { if (src_low) { - gb->registers[GB_REGISTER_AF] &= 0xFF; - gb->registers[GB_REGISTER_AF] |= value << 8; + gb->af &= 0xFF; + gb->af |= value << 8; } else { - cycle_write(gb, gb->registers[GB_REGISTER_HL], value); + cycle_write(gb, gb->hl, value); } } else { @@ -771,13 +849,13 @@ static void ld_##x##_##y(GB_gameboy_t *gb, uint8_t opcode) \ #define LD_X_DHL(x) \ static void ld_##x##_##dhl(GB_gameboy_t *gb, uint8_t opcode) \ { \ -gb->x = cycle_read(gb, gb->registers[GB_REGISTER_HL]); \ +gb->x = cycle_read(gb, gb->hl); \ } #define LD_DHL_Y(y) \ static void ld_##dhl##_##y(GB_gameboy_t *gb, uint8_t opcode) \ { \ -cycle_write(gb, gb->registers[GB_REGISTER_HL], gb->y); \ +cycle_write(gb, gb->hl, gb->y); \ } LD_X_Y(b,c) LD_X_Y(b,d) LD_X_Y(b,e) LD_X_Y(b,h) LD_X_Y(b,l) LD_X_DHL(b) LD_X_Y(b,a) @@ -801,16 +879,16 @@ static void add_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a; value = get_src_value(gb, opcode); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = (a + value) << 8; + a = gb->af >> 8; + gb->af = (a + value) << 8; if ((uint8_t)(a + value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) + (value & 0xF) > 0x0F) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (((unsigned) a) + ((unsigned) value) > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -818,18 +896,18 @@ static void adc_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a, carry; value = get_src_value(gb, opcode); - 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; + a = gb->af >> 8; + carry = (gb->af & GB_CARRY_FLAG) != 0; + gb->af = (a + value + carry) << 8; if ((uint8_t)(a + value + carry) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) + (value & 0xF) + carry > 0x0F) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (((unsigned) a) + ((unsigned) value) + carry > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -837,16 +915,16 @@ static void sub_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a; value = get_src_value(gb, opcode); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG; + a = gb->af >> 8; + gb->af = ((a - value) << 8) | GB_SUBTRACT_FLAG; if (a == value) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) < (value & 0xF)) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (a < value) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -854,18 +932,18 @@ static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a, carry; value = get_src_value(gb, opcode); - 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; + a = gb->af >> 8; + carry = (gb->af & GB_CARRY_FLAG) != 0; + gb->af = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; if ((uint8_t) (a - value - carry) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) < (value & 0xF) + carry) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (((unsigned) a) - ((unsigned) value) - carry > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -873,10 +951,10 @@ static void and_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a; value = get_src_value(gb, opcode); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG; + a = gb->af >> 8; + gb->af = ((a & value) << 8) | GB_HALF_CARRY_FLAG; if ((a & value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -884,10 +962,10 @@ static void xor_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a; value = get_src_value(gb, opcode); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = (a ^ value) << 8; + a = gb->af >> 8; + gb->af = (a ^ value) << 8; if ((a ^ value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -895,10 +973,10 @@ static void or_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a; value = get_src_value(gb, opcode); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = (a | value) << 8; + a = gb->af >> 8; + gb->af = (a | value) << 8; if ((a | value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -906,17 +984,17 @@ static void cp_a_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value, a; value = get_src_value(gb, opcode); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] &= 0xFF00; - gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + a = gb->af >> 8; + gb->af &= 0xFF00; + gb->af |= GB_SUBTRACT_FLAG; if (a == value) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) < (value & 0xF)) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (a < value) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -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]++) << 8; - gb->registers[GB_REGISTER_AF] &= 0xFFF0; // Make sure we don't set impossible flags on F! See Blargg's PUSH AF test. + gb->registers[register_id] = cycle_read(gb, gb->sp++); + gb->registers[register_id] |= cycle_read(gb, gb->sp++) << 8; + gb->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,12 +1050,12 @@ 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); - cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF); + cycle_write(gb, --gb->sp, (gb->pc) >> 8); + cycle_write(gb, --gb->sp, (gb->pc) & 0xFF); gb->pc = addr; GB_debugger_call_hook(gb, call_addr); @@ -989,130 +1067,130 @@ static void push_rr(GB_gameboy_t *gb, uint8_t opcode) uint8_t register_id; cycle_oam_bug(gb, GB_REGISTER_SP); register_id = ((opcode >> 4) + 1) & 3; - cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->registers[register_id]) >> 8); - cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->registers[register_id]) & 0xFF); + cycle_write(gb, --gb->sp, (gb->registers[register_id]) >> 8); + cycle_write(gb, --gb->sp, (gb->registers[register_id]) & 0xFF); } 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++); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = (a + value) << 8; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + gb->af = (a + value) << 8; if ((uint8_t) (a + value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) + (value & 0xF) > 0x0F) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (((unsigned) a) + ((unsigned) value) > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } 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++); - 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; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + carry = (gb->af & GB_CARRY_FLAG) != 0; + gb->af = (a + value + carry) << 8; - if (gb->registers[GB_REGISTER_AF] == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + if (gb->af == 0) { + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) + (value & 0xF) + carry > 0x0F) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (((unsigned) a) + ((unsigned) value) + carry > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } 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++); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + gb->af = ((a - value) << 8) | GB_SUBTRACT_FLAG; if (a == value) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) < (value & 0xF)) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (a < value) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } 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++); - 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; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + carry = (gb->af & GB_CARRY_FLAG) != 0; + gb->af = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; if ((uint8_t) (a - value - carry) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) < (value & 0xF) + carry) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (((unsigned) a) - ((unsigned) value) - carry > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } 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++); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + gb->af = ((a & value) << 8) | GB_HALF_CARRY_FLAG; if ((a & value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } 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++); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = (a ^ value) << 8; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + gb->af = (a ^ value) << 8; if ((a ^ value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } 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++); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] = (a | value) << 8; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + gb->af = (a | value) << 8; if ((a | value) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } 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++); - a = gb->registers[GB_REGISTER_AF] >> 8; - gb->registers[GB_REGISTER_AF] &= 0xFF00; - gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + value = cycle_read(gb, gb->pc++); + a = gb->af >> 8; + gb->af &= 0xFF00; + gb->af |= GB_SUBTRACT_FLAG; if (a == value) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } if ((a & 0xF) < (value & 0xF)) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if (a < value) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } @@ -1120,8 +1198,8 @@ static void rst(GB_gameboy_t *gb, uint8_t opcode) { uint16_t call_addr = gb->pc - 1; 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); + cycle_write(gb, --gb->sp, (gb->pc) >> 8); + cycle_write(gb, --gb->sp, (gb->pc) & 0xFF); gb->pc = opcode ^ 0xC7; GB_debugger_call_hook(gb, call_addr); } @@ -1129,8 +1207,8 @@ 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]++) << 8; + gb->pc = cycle_read(gb, gb->sp++); + gb->pc |= cycle_read(gb, gb->sp++) << 8; cycle_no_access(gb); } @@ -1154,80 +1232,80 @@ 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); + cycle_write(gb, --gb->sp, (gb->pc) >> 8); + cycle_write(gb, --gb->sp, (gb->pc) & 0xFF); gb->pc = addr; GB_debugger_call_hook(gb, call_addr); } static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode) { - uint8_t temp = cycle_read_inc_oam_bug(gb, gb->pc++); - cycle_write(gb, 0xFF00 + temp, gb->registers[GB_REGISTER_AF] >> 8); + uint8_t temp = cycle_read(gb, gb->pc++); + cycle_write(gb, 0xFF00 + temp, gb->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++); - gb->registers[GB_REGISTER_AF] |= cycle_read(gb, 0xFF00 + temp) << 8; + gb->af &= 0xFF; + uint8_t temp = cycle_read(gb, gb->pc++); + gb->af |= cycle_read(gb, 0xFF00 + temp) << 8; } static void ld_dc_a(GB_gameboy_t *gb, uint8_t opcode) { - cycle_write(gb, 0xFF00 + (gb->registers[GB_REGISTER_BC] & 0xFF), gb->registers[GB_REGISTER_AF] >> 8); + cycle_write(gb, 0xFF00 + (gb->bc & 0xFF), gb->af >> 8); } static void ld_a_dc(GB_gameboy_t *gb, uint8_t opcode) { - gb->registers[GB_REGISTER_AF] &= 0xFF; - gb->registers[GB_REGISTER_AF] |= cycle_read(gb, 0xFF00 + (gb->registers[GB_REGISTER_BC] & 0xFF)) << 8; + gb->af &= 0xFF; + gb->af |= cycle_read(gb, 0xFF00 + (gb->bc & 0xFF)) << 8; } 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++); + uint16_t sp = gb->sp; + offset = (int8_t) cycle_read(gb, gb->pc++); cycle_no_access(gb); cycle_no_access(gb); - gb->registers[GB_REGISTER_SP] += offset; + gb->sp += offset; - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; /* A new instruction, a new meaning for Half Carry! */ if ((sp & 0xF) + (offset & 0xF) > 0xF) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; } if ((sp & 0xFF) + (offset & 0xFF) > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } } static void jp_hl(GB_gameboy_t *gb, uint8_t opcode) { - gb->pc = gb->registers[GB_REGISTER_HL]; + gb->pc = gb->hl; } 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; - cycle_write(gb, addr, gb->registers[GB_REGISTER_AF] >> 8); + addr = cycle_read(gb, gb->pc++); + addr |= cycle_read(gb, gb->pc++) << 8; + cycle_write(gb, addr, gb->af >> 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; - gb->registers[GB_REGISTER_AF] |= cycle_read(gb, addr) << 8; + gb->af &= 0xFF; + addr = cycle_read(gb, gb->pc++); + addr |= cycle_read(gb, gb->pc++) << 8; + gb->af |= cycle_read(gb, addr) << 8; } static void di(GB_gameboy_t *gb, uint8_t opcode) @@ -1248,24 +1326,24 @@ static void ei(GB_gameboy_t *gb, uint8_t opcode) 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++); + gb->af &= 0xFF00; + offset = (int8_t) cycle_read(gb, gb->pc++); cycle_no_access(gb); - gb->registers[GB_REGISTER_HL] = gb->registers[GB_REGISTER_SP] + offset; + gb->hl = gb->sp + offset; - if ((gb->registers[GB_REGISTER_SP] & 0xF) + (offset & 0xF) > 0xF) { - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + if ((gb->sp & 0xF) + (offset & 0xF) > 0xF) { + gb->af |= GB_HALF_CARRY_FLAG; } - if ((gb->registers[GB_REGISTER_SP] & 0xFF) + (offset & 0xFF) > 0xFF) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + if ((gb->sp & 0xFF) + (offset & 0xFF) > 0xFF) { + gb->af |= GB_CARRY_FLAG; } } static void ld_sp_hl(GB_gameboy_t *gb, uint8_t opcode) { - gb->registers[GB_REGISTER_SP] = gb->registers[GB_REGISTER_HL]; - cycle_no_access(gb); + gb->sp = gb->hl; + cycle_oam_bug(gb, GB_REGISTER_HL); } static void rlc_r(GB_gameboy_t *gb, uint8_t opcode) @@ -1274,13 +1352,13 @@ static void rlc_r(GB_gameboy_t *gb, uint8_t opcode) uint8_t value; value = get_src_value(gb, opcode); carry = (value & 0x80) != 0; - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; set_src_value(gb, opcode, (value << 1) | carry); if (carry) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } if (!(value << 1)) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1290,14 +1368,14 @@ static void rrc_r(GB_gameboy_t *gb, uint8_t opcode) uint8_t value; value = get_src_value(gb, opcode); carry = (value & 0x01) != 0; - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; value = (value >> 1) | (carry << 7); set_src_value(gb, opcode, value); if (carry) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } if (value == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1307,17 +1385,17 @@ static void rl_r(GB_gameboy_t *gb, uint8_t opcode) uint8_t value; bool bit7; value = get_src_value(gb, opcode); - carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + carry = (gb->af & GB_CARRY_FLAG) != 0; bit7 = (value & 0x80) != 0; - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; value = (value << 1) | carry; set_src_value(gb, opcode, value); if (bit7) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } if (value == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1328,17 +1406,17 @@ static void rr_r(GB_gameboy_t *gb, uint8_t opcode) bool bit1; value = get_src_value(gb, opcode); - carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + carry = (gb->af & GB_CARRY_FLAG) != 0; bit1 = (value & 0x1) != 0; - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; value = (value >> 1) | (carry << 7); set_src_value(gb, opcode, value); if (bit1) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } if (value == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1348,13 +1426,13 @@ static void sla_r(GB_gameboy_t *gb, uint8_t opcode) bool carry; value = get_src_value(gb, opcode); carry = (value & 0x80) != 0; - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; set_src_value(gb, opcode, (value << 1)); if (carry) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } if ((value & 0x7F) == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1364,14 +1442,14 @@ static void sra_r(GB_gameboy_t *gb, uint8_t opcode) uint8_t value; value = get_src_value(gb, opcode); bit7 = value & 0x80; - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; if (value & 1) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } value = (value >> 1) | bit7; set_src_value(gb, opcode, value); if (value == 0) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1379,13 +1457,13 @@ static void srl_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value; value = get_src_value(gb, opcode); - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; set_src_value(gb, opcode, (value >> 1)); if (value & 1) { - gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->af |= GB_CARRY_FLAG; } if (!(value >> 1)) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1393,10 +1471,10 @@ static void swap_r(GB_gameboy_t *gb, uint8_t opcode) { uint8_t value; value = get_src_value(gb, opcode); - gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->af &= 0xFF00; set_src_value(gb, opcode, (value >> 4) | (value << 4)); if (!value) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } @@ -1407,10 +1485,10 @@ static void bit_r(GB_gameboy_t *gb, uint8_t opcode) value = get_src_value(gb, opcode); bit = 1 << ((opcode >> 3) & 7); if ((opcode & 0xC0) == 0x40) { /* Bit */ - gb->registers[GB_REGISTER_AF] &= 0xFF00 | GB_CARRY_FLAG; - gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + gb->af &= 0xFF00 | GB_CARRY_FLAG; + gb->af |= GB_HALF_CARRY_FLAG; if (!(bit & value)) { - gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + gb->af |= GB_ZERO_FLAG; } } else if ((opcode & 0xC0) == 0x80) { /* res */ @@ -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); @@ -1455,9 +1533,9 @@ 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 */ +static opcode_t *opcodes[256] = { +/* 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 */ @@ -1500,6 +1578,9 @@ void GB_cpu_run(GB_gameboy_t *gb) if (gb->stopped) { GB_timing_sync(gb); GB_advance_cycles(gb, 4); + if ((gb->io_registers[GB_IO_JOYP] & 0x30) != 0x30) { + gb->joyp_accessed = true; + } if ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF) { leave_stop_mode(gb); GB_advance_cycles(gb, 8); @@ -1531,27 +1612,30 @@ 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_trigger_oam_bug(gb, gb->registers[GB_REGISTER_SP]); /* Todo: test T-cycle timing */ + gb->last_opcode_read = cycle_read(gb, gb->pc++); + cycle_oam_bug_pc(gb); + gb->pc--; + GB_trigger_oam_bug(gb, gb->sp); /* Todo: test T-cycle timing */ cycle_no_access(gb); - cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8); + cycle_write(gb, --gb->sp, (gb->pc) >> 8); interrupt_queue = gb->interrupt_enable; - if (gb->registers[GB_REGISTER_SP] == GB_IO_IF + 0xFF00 + 1) { - gb->registers[GB_REGISTER_SP]--; + if (gb->sp == GB_IO_IF + 0xFF00 + 1) { + gb->sp--; interrupt_queue &= cycle_write_if(gb, (gb->pc) & 0xFF); } else { - cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF); + cycle_write(gb, --gb->sp, (gb->pc) & 0xFF); interrupt_queue &= (gb->io_registers[GB_IO_IF]) & 0x1F; } @@ -1561,6 +1645,10 @@ void GB_cpu_run(GB_gameboy_t *gb) interrupt_queue >>= 1; interrupt_bit++; } + assert(gb->pending_cycles > 2); + gb->pending_cycles -= 2; + flush_pending_cycles(gb); + gb->pending_cycles = 2; gb->io_registers[GB_IO_IF] &= ~(1 << interrupt_bit); gb->pc = interrupt_bit * 8 + 0x40; } @@ -1572,8 +1660,11 @@ 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++); - if (gb->halt_bug) { + gb->last_opcode_read = cycle_read(gb, gb->pc++); + if (unlikely(gb->execution_callback)) { + gb->execution_callback(gb, gb->pc - 1, gb->last_opcode_read); + } + if (unlikely(gb->halt_bug)) { gb->pc--; gb->halt_bug = false; } diff --git a/Core/sm83_cpu.h b/Core/sm83_cpu.h index 49fa80b..1221fd7 100644 --- a/Core/sm83_cpu.h +++ b/Core/sm83_cpu.h @@ -1,11 +1,11 @@ #ifndef sm83_cpu_h #define sm83_cpu_h -#include "gb_struct_def.h" +#include "defs.h" #include void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count); #ifdef GB_INTERNAL -void GB_cpu_run(GB_gameboy_t *gb); +internal void GB_cpu_run(GB_gameboy_t *gb); #endif #endif /* sm83_cpu_h */ diff --git a/Core/sm83_disassembler.c b/Core/sm83_disassembler.c index 7dacd9e..f85bfc2 100644 --- a/Core/sm83_disassembler.c +++ b/Core/sm83_disassembler.c @@ -2,8 +2,9 @@ #include #include "gb.h" +#define GB_read_memory GB_safe_read_memory -typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc); +typedef void opcode_t(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc); static void ill(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) { @@ -716,7 +717,7 @@ static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) } } -static GB_opcode_t *opcodes[256] = { +static opcode_t *opcodes[256] = { /* 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 */ diff --git a/Core/symbol_hash.c b/Core/symbol_hash.c index 75a7837..66894f1 100644 --- a/Core/symbol_hash.c +++ b/Core/symbol_hash.c @@ -4,7 +4,7 @@ #include #include -static size_t GB_map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr) +static size_t map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr) { if (!map->symbols) { return 0; @@ -26,7 +26,7 @@ static size_t GB_map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr) GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name) { - size_t index = GB_map_find_symbol_index(map, addr); + size_t index = map_find_symbol_index(map, addr); map->symbols = realloc(map->symbols, (map->n_symbols + 1) * sizeof(map->symbols[0])); memmove(&map->symbols[index + 1], &map->symbols[index], (map->n_symbols - index) * sizeof(map->symbols[0])); @@ -39,8 +39,8 @@ GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const c 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) { + size_t index = map_find_symbol_index(map, addr); + if (index >= map->n_symbols || map->symbols[index].addr != addr) { index--; } if (index < map->n_symbols) { diff --git a/Core/symbol_hash.h b/Core/symbol_hash.h index 2a03c96..d063312 100644 --- a/Core/symbol_hash.h +++ b/Core/symbol_hash.h @@ -27,12 +27,12 @@ typedef struct { } GB_reversed_symbol_map_t; #ifdef GB_INTERNAL -void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *symbol); -const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name); -GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name); -const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr); -GB_symbol_map_t *GB_map_alloc(void); -void GB_map_free(GB_symbol_map_t *map); +internal void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *symbol); +internal const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name); +internal GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name); +internal const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr); +internal GB_symbol_map_t *GB_map_alloc(void); +internal void GB_map_free(GB_symbol_map_t *map); #endif #endif /* symbol_hash_h */ diff --git a/Core/timing.c b/Core/timing.c index 965ba27..c5d328f 100644 --- a/Core/timing.c +++ b/Core/timing.c @@ -8,7 +8,7 @@ #include #endif -static const unsigned GB_TAC_TRIGGER_BITS[] = {512, 8, 32, 128}; +static const unsigned TAC_TRIGGER_BITS[] = {512, 8, 32, 128}; #ifndef GB_DISABLE_TIMEKEEPING static int64_t get_nanoseconds(void) @@ -54,21 +54,30 @@ bool GB_timing_sync_turbo(GB_gameboy_t *gb) void GB_timing_sync(GB_gameboy_t *gb) { - if (gb->turbo) { - gb->cycles_since_last_sync = 0; - return; - } /* Prevent syncing if not enough time has passed.*/ if (gb->cycles_since_last_sync < LCDC_PERIOD / 3) return; + if (gb->turbo) { + gb->cycles_since_last_sync = 0; + if (gb->update_input_hint_callback) { + gb->update_input_hint_callback(gb); + } + return; + } + 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; } @@ -86,18 +95,43 @@ bool GB_timing_sync_turbo(GB_gameboy_t *gb) void GB_timing_sync(GB_gameboy_t *gb) { + if (gb->cycles_since_last_sync < LCDC_PERIOD / 3) return; + gb->cycles_since_last_sync = 0; + + gb->cycles_since_last_sync = 0; + if (gb->update_input_hint_callback) { + gb->update_input_hint_callback(gb); + } + return; } #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 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 || !gb->cgb_mode) && gb->cartridge_type->mbc_type != GB_HUC1 && gb->cartridge_type->mbc_type != GB_HUC3) 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,38 +154,42 @@ static void increase_tima(GB_gameboy_t *gb) } } -static void GB_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value) +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; - if ((gb->io_registers[GB_IO_TAC] & 4) && (triggers & GB_TAC_TRIGGER_BITS[gb->io_registers[GB_IO_TAC] & 3])) { + uint16_t triggers = gb->div_counter & ~value; + if ((gb->io_registers[GB_IO_TAC] & 4) && (triggers & 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)) { - GB_apu_run(gb); + uint16_t apu_bit = gb->cgb_double_speed? 0x2000 : 0x1000; + if (triggers & apu_bit) { GB_apu_div_event(gb); } + else { + uint16_t secondary_triggers = ~gb->div_counter & value; + if (secondary_triggers & apu_bit) { + GB_apu_div_secondary_event(gb); + } + } gb->div_counter = value; } -static void GB_timers_run(GB_gameboy_t *gb, uint8_t cycles) +static void 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; } GB_STATE_MACHINE(gb, div, cycles, 1) { GB_STATE(gb, div, 1); GB_STATE(gb, div, 2); - GB_STATE(gb, div, 3); } - GB_set_internal_div_counter(gb, 0); -main: GB_SLEEP(gb, div, 1, 3); while (true) { advance_tima_state_machine(gb); @@ -159,19 +197,14 @@ main: gb->apu.apu_cycles += 4 << !gb->cgb_double_speed; GB_SLEEP(gb, div, 2, 4); } - - /* Todo: This is ugly to allow compatibility with 0.11 save states. Fix me when breaking save compatibility */ - { - div3: - /* Compensate for lack of prefetch emulation, as well as DIV's internal initial value */ - GB_set_internal_div_counter(gb, 8); - goto main; - } } static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) { - if (gb->serial_length == 0) { + if (unlikely(gb->printer_callback && (gb->printer.command_state || gb->printer.bits_received))) { + gb->printer.idle_time += cycles; + } + if (likely(gb->serial_length == 0)) { gb->serial_cycles += cycles; return; } @@ -213,46 +246,196 @@ static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) } +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_set_rtc_multiplier(GB_gameboy_t *gb, double multiplier) +{ + if (multiplier == 1) { + gb->rtc_second_length = 0; + return; + } + + gb->rtc_second_length = GB_get_unmultiplied_clock_rate(gb) * 2 * multiplier; +} + +static void rtc_run(GB_gameboy_t *gb, uint8_t cycles) +{ + if (likely(gb->cartridge_type->mbc_type != GB_HUC3 && !gb->cartridge_type->has_rtc)) return; + gb->rtc_cycles += cycles; + time_t current_time = 0; + uint32_t rtc_second_length = unlikely(gb->rtc_second_length)? gb->rtc_second_length : GB_get_unmultiplied_clock_rate(gb) * 2; + + 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 < rtc_second_length) return; + gb->rtc_cycles -= rtc_second_length; + 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 (unlikely(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; - GB_timers_run(gb, cycles); - if (!gb->stopped) { + timers_run(gb, cycles); + if (unlikely(!gb->stopped)) { advance_serial(gb, cycles); // TODO: Verify what happens in STOP mode } + if (unlikely(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) { + if (unlikely(!gb->cgb_double_speed)) { cycles <<= 1; } + gb->absolute_debugger_ticks += cycles; + // Not affected by speed boost - gb->double_speed_alignment += cycles; + if (likely(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->apu_output.sample_cycles += cycles * gb->apu_output.sample_rate; gb->cycles_since_last_sync += cycles; gb->cycles_since_run += cycles; - if (gb->rumble_state) { - gb->rumble_on_cycles++; - } - else { - gb->rumble_off_cycles++; - } - - if (!gb->stopped) { // TODO: Verify what happens in STOP mode + gb->rumble_on_cycles += gb->rumble_strength & 3; + gb->rumble_off_cycles += (gb->rumble_strength & 3) ^ 3; + + if (unlikely(!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_apu_run(gb, false); + GB_display_run(gb, cycles, false); + ir_run(gb, cycles); + rtc_run(gb, cycles); } /* @@ -265,63 +448,14 @@ void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac) /* Glitch only happens when old_tac is enabled. */ if (!(old_tac & 4)) return; - unsigned old_clocks = GB_TAC_TRIGGER_BITS[old_tac & 3]; - unsigned new_clocks = GB_TAC_TRIGGER_BITS[new_tac & 3]; + unsigned old_clocks = TAC_TRIGGER_BITS[old_tac & 3]; + unsigned new_clocks = TAC_TRIGGER_BITS[new_tac & 3]; /* The bit used for overflow testing must have been 1 */ if (gb->div_counter & old_clocks) { /* And now either the timer must be disabled, or the new bit used for overflow testing be 0. */ - if (!(new_tac & 4) || gb->div_counter & new_clocks) { + if (!(new_tac & 4) || !(gb->div_counter & new_clocks)) { increase_tima(gb); } } } - -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/Core/timing.h b/Core/timing.h index d4fa07f..ee817d1 100644 --- a/Core/timing.h +++ b/Core/timing.h @@ -1,14 +1,24 @@ #ifndef timing_h #define timing_h -#include "gb_struct_def.h" +#include "defs.h" + +typedef enum { + GB_RTC_MODE_SYNC_TO_HOST, + GB_RTC_MODE_ACCURATE, +} GB_rtc_mode_t; + +/* RTC emulation mode */ +void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode); + +/* Speed multiplier for the RTC, mostly for TAS syncing */ +void GB_set_rtc_multiplier(GB_gameboy_t *gb, double multiplier); #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); - +internal void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles); +internal void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac); +internal bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */ +internal void GB_timing_sync(GB_gameboy_t *gb); +internal void GB_set_internal_div_counter(GB_gameboy_t *gb, uint16_t value); enum { GB_TIMA_RUNNING = 0, GB_TIMA_RELOADING = 1, @@ -18,13 +28,23 @@ enum { #define GB_SLEEP(gb, unit, state, cycles) do {\ (gb)->unit##_cycles -= (cycles) * __state_machine_divisor; \ - if ((gb)->unit##_cycles <= 0) {\ + if (unlikely((gb)->unit##_cycles <= 0)) {\ (gb)->unit##_state = state;\ return;\ unit##state:; \ }\ } while (0) +#define GB_BATCHPOINT(gb, unit, state, cycles) do {\ +unit##state:; \ +if (likely(__state_machine_allow_batching && (gb)->unit##_cycles < (cycles * 2))) {\ + (gb)->unit##_state = state;\ + return;\ +}\ +} while (0) + +#define GB_BATCHED_CYCLES(gb, unit) ((gb)->unit##_cycles / __state_machine_divisor) + #define GB_STATE_MACHINE(gb, unit, cycles, divisor) \ static const int __state_machine_divisor = divisor;\ (gb)->unit##_cycles += cycles; \ @@ -34,6 +54,10 @@ if ((gb)->unit##_cycles <= 0) {\ switch ((gb)->unit##_state) #endif +#define GB_BATCHABLE_STATE_MACHINE(gb, unit, cycles, divisor, allow_batching) \ +const bool __state_machine_allow_batching = (allow_batching); \ +GB_STATE_MACHINE(gb, unit, cycles, divisor) + #define GB_STATE(gb, unit, state) case state: goto unit##state #define GB_UNIT(unit) int32_t unit##_cycles, unit##_state diff --git a/Core/workboy.h b/Core/workboy.h index d21f273..c99c272 100644 --- a/Core/workboy.h +++ b/Core/workboy.h @@ -3,7 +3,7 @@ #include #include #include -#include "gb_struct_def.h" +#include "defs.h" typedef struct { diff --git a/FreeDesktop/AppIcon/128x128.png b/FreeDesktop/AppIcon/128x128.png new file mode 100644 index 0000000..6303f23 Binary files /dev/null and b/FreeDesktop/AppIcon/128x128.png differ diff --git a/FreeDesktop/AppIcon/16x16.png b/FreeDesktop/AppIcon/16x16.png new file mode 100644 index 0000000..6c3f81d Binary files /dev/null and b/FreeDesktop/AppIcon/16x16.png differ diff --git a/FreeDesktop/AppIcon/256x256.png b/FreeDesktop/AppIcon/256x256.png new file mode 100644 index 0000000..e2a6cee Binary files /dev/null and b/FreeDesktop/AppIcon/256x256.png differ diff --git a/FreeDesktop/AppIcon/32x32.png b/FreeDesktop/AppIcon/32x32.png new file mode 100644 index 0000000..d7f2e4e Binary files /dev/null and b/FreeDesktop/AppIcon/32x32.png differ diff --git a/FreeDesktop/AppIcon/512x512.png b/FreeDesktop/AppIcon/512x512.png new file mode 100644 index 0000000..1608c71 Binary files /dev/null and b/FreeDesktop/AppIcon/512x512.png differ diff --git a/FreeDesktop/AppIcon/64x64.png b/FreeDesktop/AppIcon/64x64.png new file mode 100644 index 0000000..4a54e94 Binary files /dev/null and b/FreeDesktop/AppIcon/64x64.png differ diff --git a/FreeDesktop/Cartridge/128x128.png b/FreeDesktop/Cartridge/128x128.png new file mode 100644 index 0000000..bc14d79 Binary files /dev/null and b/FreeDesktop/Cartridge/128x128.png differ diff --git a/FreeDesktop/Cartridge/16x16.png b/FreeDesktop/Cartridge/16x16.png new file mode 100644 index 0000000..3cbd9ae Binary files /dev/null and b/FreeDesktop/Cartridge/16x16.png differ diff --git a/FreeDesktop/Cartridge/256x256.png b/FreeDesktop/Cartridge/256x256.png new file mode 100644 index 0000000..14258ea Binary files /dev/null and b/FreeDesktop/Cartridge/256x256.png differ diff --git a/FreeDesktop/Cartridge/32x32.png b/FreeDesktop/Cartridge/32x32.png new file mode 100644 index 0000000..c8ef62f Binary files /dev/null and b/FreeDesktop/Cartridge/32x32.png differ diff --git a/FreeDesktop/Cartridge/512x512.png b/FreeDesktop/Cartridge/512x512.png new file mode 100644 index 0000000..71314f7 Binary files /dev/null and b/FreeDesktop/Cartridge/512x512.png differ diff --git a/FreeDesktop/Cartridge/64x64.png b/FreeDesktop/Cartridge/64x64.png new file mode 100644 index 0000000..8835f79 Binary files /dev/null and b/FreeDesktop/Cartridge/64x64.png differ diff --git a/FreeDesktop/ColorCartridge/128x128.png b/FreeDesktop/ColorCartridge/128x128.png new file mode 100644 index 0000000..da4757e Binary files /dev/null and b/FreeDesktop/ColorCartridge/128x128.png differ diff --git a/FreeDesktop/ColorCartridge/16x16.png b/FreeDesktop/ColorCartridge/16x16.png new file mode 100644 index 0000000..50e6b2b Binary files /dev/null and b/FreeDesktop/ColorCartridge/16x16.png differ diff --git a/FreeDesktop/ColorCartridge/256x256.png b/FreeDesktop/ColorCartridge/256x256.png new file mode 100644 index 0000000..186f5d3 Binary files /dev/null and b/FreeDesktop/ColorCartridge/256x256.png differ diff --git a/FreeDesktop/ColorCartridge/32x32.png b/FreeDesktop/ColorCartridge/32x32.png new file mode 100644 index 0000000..47e45b5 Binary files /dev/null and b/FreeDesktop/ColorCartridge/32x32.png differ diff --git a/FreeDesktop/ColorCartridge/512x512.png b/FreeDesktop/ColorCartridge/512x512.png new file mode 100644 index 0000000..715d68f Binary files /dev/null and b/FreeDesktop/ColorCartridge/512x512.png differ diff --git a/FreeDesktop/ColorCartridge/64x64.png b/FreeDesktop/ColorCartridge/64x64.png new file mode 100644 index 0000000..403e307 Binary files /dev/null and b/FreeDesktop/ColorCartridge/64x64.png differ diff --git a/FreeDesktop/sameboy.desktop b/FreeDesktop/sameboy.desktop new file mode 100644 index 0000000..80d0902 --- /dev/null +++ b/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/FreeDesktop/sameboy.xml b/FreeDesktop/sameboy.xml new file mode 100644 index 0000000..18123ed --- /dev/null +++ b/FreeDesktop/sameboy.xml @@ -0,0 +1,23 @@ + + + + Game Boy ROM + + + + + + + Game Boy Color ROM + + + + + + + Game Boy ISX binary + + + + + diff --git a/JoyKit/ControllerConfiguration.inc b/JoyKit/ControllerConfiguration.inc index ea3ba9a..86988c7 100644 --- a/JoyKit/ControllerConfiguration.inc +++ b/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,65 @@ 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}, + + @{@"reportID": @(0x31), @"size":@16, @"offset":@0x80, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0x90, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xA0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xB0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisX)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xC0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisY)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xD0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ)}, + ], + + @(1): @[ + @{@"reportID": @(1), @"size":@16, @"offset":@0x78, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0x88, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0x98, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0xA8, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisX)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0xB8, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisY)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0xC8, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ)}, + ], + }, + + JOYIsSony: @YES, } }; @@ -371,8 +422,6 @@ hacksByName = @{ JOYCustomReports: @{ @(0x30): @[ - // For USB mode, which uses the wrong report descriptor - @{@"reportID": @(1), @"size":@1, @"offset":@16, @"usagePage":@(kHIDPage_Button), @"usage":@3}, @{@"reportID": @(1), @"size":@1, @"offset":@17, @"usagePage":@(kHIDPage_Button), @"usage":@4}, @{@"reportID": @(1), @"size":@1, @"offset":@18, @"usagePage":@(kHIDPage_Button), @"usage":@1}, @@ -405,12 +454,18 @@ hacksByName = @{ @{@"reportID": @(1), @"size":@12, @"offset":@64, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @0xFFF}, @{@"reportID": @(1), @"size":@12, @"offset":@76, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0xFFF, @"max": @0}, + + @{@"reportID": @(1), @"size":@16, @"offset":@96, @"usagePage":@(kHIDPage_Sensor), @"min": @0x7FFF, @"max": @-0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ)}, + @{@"reportID": @(1), @"size":@16, @"offset":@112, @"usagePage":@(kHIDPage_Sensor), @"min": @0x7FFF, @"max": @-0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisX)}, + @{@"reportID": @(1), @"size":@16, @"offset":@128, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisY)}, + @{@"reportID": @(1), @"size":@16, @"offset":@144, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@160, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@176, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis)}, ], }, + + 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/JoyKit/JOYAxes3D.h b/JoyKit/JOYAxes3D.h new file mode 100644 index 0000000..5c83807 --- /dev/null +++ b/JoyKit/JOYAxes3D.h @@ -0,0 +1,27 @@ +#import + +typedef enum { + JOYAxes3DUsageNone, + JOYAxes3DUsageAcceleration, + JOYAxes3DUsageOrientation, + JOYAxes3DUsageGyroscope, + JOYAxes3DUsageNonGenericMax, + + JOYAxes3DUsageGeneric0 = 0x10000, +} JOYAxes3DUsage; + +typedef struct { + double x, y, z; +} JOYPoint3D; + +@interface JOYAxes3D : NSObject +- (NSString *)usageString; ++ (NSString *)usageToString: (JOYAxes3DUsage) usage; +- (uint64_t)uniqueID; +- (JOYPoint3D)rawValue; +- (JOYPoint3D)normalizedValue; // For orientation +- (JOYPoint3D)gUnitsValue; // For acceleration +@property JOYAxes3DUsage usage; +@end + + diff --git a/JoyKit/JOYAxes3D.m b/JoyKit/JOYAxes3D.m new file mode 100644 index 0000000..6ec146a --- /dev/null +++ b/JoyKit/JOYAxes3D.m @@ -0,0 +1,108 @@ +#import "JOYAxes3D.h" +#import "JOYElement.h" + +@implementation JOYAxes3D +{ + JOYElement *_element1, *_element2, *_element3; + double _state1, _state2, _state3; + int32_t _minX, _minY, _minZ; + int32_t _maxX, _maxY, _maxZ; + double _gApproximation; +} + ++ (NSString *)usageToString: (JOYAxes3DUsage) usage +{ + if (usage < JOYAxes3DUsageNonGenericMax) { + return (NSString *[]) { + @"None", + @"Acceleretion", + @"Orientation", + @"Gyroscope", + }[usage]; + } + if (usage >= JOYAxes3DUsageGeneric0) { + return [NSString stringWithFormat:@"Generic 3D Analog Control %d", usage - JOYAxes3DUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage 3D Axes %d", usage]; +} + +- (NSString *)usageString +{ + return [self.class usageToString:_usage]; +} + +- (uint64_t)uniqueID +{ + return _element1.uniqueID; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: (%.2f, %.2f, %.2f)>", self.className, self, self.usageString, self.uniqueID, _state1, _state2, _state3]; +} + +- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2 thirdElement:(JOYElement *)element3 +{ + self = [super init]; + if (!self) return self; + + _element1 = element1; + _element2 = element2; + _element3 = element3; + + _maxX = element1? element1.max : 1; + _maxY = element2? element2.max : 1; + _maxZ = element3? element3.max : 1; + _minX = element1? element1.min : -1; + _minY = element2? element2.min : -1; + _minZ = element3? element3.min : -1; + + return self; +} + +- (JOYPoint3D)rawValue +{ + return (JOYPoint3D){_state1, _state2, _state3}; +} + +- (JOYPoint3D)normalizedValue +{ + double distance = sqrt(_state1 * _state1 + _state2 * _state2 + _state3 * _state3); + if (distance == 0) { + distance = 1; + } + return (JOYPoint3D){_state1 / distance, _state2 / distance, _state3 / distance}; +} + +- (JOYPoint3D)gUnitsValue +{ + double distance = _gApproximation ?: 1; + return (JOYPoint3D){_state1 / distance, _state2 / distance, _state3 / distance}; +} + +- (bool)updateState +{ + int32_t x = [_element1 value]; + int32_t y = [_element2 value]; + int32_t z = [_element3 value]; + + if (x == 0 && y == 0 && z == 0) return false; + + double old1 = _state1, old2 = _state2, old3 = _state3; + _state1 = (x - _minX) / (double)(_maxX - _minX) * 2 - 1; + _state2 = (y - _minY) / (double)(_maxY - _minY) * 2 - 1; + _state3 = (z - _minZ) / (double)(_maxZ - _minZ) * 2 - 1; + + double distance = sqrt(_state1 * _state1 + _state2 * _state2 + _state3 * _state3); + if (_gApproximation == 0) { + _gApproximation = distance; + } + else { + _gApproximation = _gApproximation * 0.9999 + distance * 0.0001; + } + + return old1 != _state1 || old2 != _state2 || old3 != _state3; +} + +@end diff --git a/JoyKit/JOYAxis.h b/JoyKit/JOYAxis.h index 5a4c166..8d4b7ab 100644 --- a/JoyKit/JOYAxis.h +++ b/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/JoyKit/JOYAxis.m b/JoyKit/JOYAxis.m index 169eaee..afe90d2 100644 --- a/JoyKit/JOYAxis.m +++ b/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/JoyKit/JOYButton.h b/JoyKit/JOYButton.h index f732c8e..08c3ace 100644 --- a/JoyKit/JOYButton.h +++ b/JoyKit/JOYButton.h @@ -1,7 +1,5 @@ #import - - typedef enum { JOYButtonUsageNone, JOYButtonUsageA, @@ -26,17 +24,35 @@ typedef enum { JOYButtonUsageDPadRight, JOYButtonUsageDPadUp, JOYButtonUsageDPadDown, + + JOYButtonUsageSlider, + JOYButtonUsageDial, + JOYButtonUsageWheel, + + JOYButtonUsageRudder, + JOYButtonUsageThrottle, + JOYButtonUsageAccelerator, + JOYButtonUsageBrake, + JOYButtonUsageNonGenericMax, JOYButtonUsageGeneric0 = 0x10000, } JOYButtonUsage; +typedef enum { + JOYButtonTypeNormal, + JOYButtonTypeAxisEmulated, + JOYButtonTypeAxes2DEmulated, + JOYButtonTypeHatEmulated, +} JOYButtonType; + @interface JOYButton : NSObject - (NSString *)usageString; + (NSString *)usageToString: (JOYButtonUsage) usage; - (uint64_t)uniqueID; - (bool) isPressed; @property JOYButtonUsage usage; +@property (readonly) JOYButtonType type; @end diff --git a/JoyKit/JOYButton.m b/JoyKit/JOYButton.m index 3e6026d..568d383 100644 --- a/JoyKit/JOYButton.m +++ b/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; } @@ -99,4 +106,8 @@ return false; } +- (JOYButtonType)type +{ + return JOYButtonTypeNormal; +} @end diff --git a/JoyKit/JOYController.h b/JoyKit/JOYController.h index 9ed7cf7..a21175c 100644 --- a/JoyKit/JOYController.h +++ b/JoyKit/JOYController.h @@ -2,9 +2,9 @@ #import "JOYButton.h" #import "JOYAxis.h" #import "JOYAxes2D.h" +#import "JOYAxes3D.h" #import "JOYHat.h" -static NSString const *JOYAxesEmulateButtonsKey = @"JOYAxesEmulateButtons"; static NSString const *JOYAxes2DEmulateButtonsKey = @"JOYAxes2DEmulateButtons"; static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; @@ -18,6 +18,7 @@ static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; -(void) controller:(JOYController *)controller buttonChangedState:(JOYButton *)button; -(void) controller:(JOYController *)controller movedAxis:(JOYAxis *)axis; -(void) controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes; +-(void) controller:(JOYController *)controller movedAxes3D:(JOYAxes3D *)axes; -(void) controller:(JOYController *)controller movedHat:(JOYHat *)hat; @end @@ -32,9 +33,11 @@ static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; - (NSArray *) buttons; - (NSArray *) axes; - (NSArray *) axes2D; +- (NSArray *) axes3D; - (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/JoyKit/JOYController.m b/JoyKit/JOYController.m index ca2d1b1..caae2cc 100644 --- a/JoyKit/JOYController.m +++ b/JoyKit/JOYController.m @@ -3,10 +3,13 @@ #import "JOYElement.h" #import "JOYSubElement.h" #import "JOYFullReportElement.h" - +#import "JOYButton.h" #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; @@ -67,6 +71,14 @@ static bool hatsEmulateButtons = false; - (bool)updateState; @end +@interface JOYAxes3D () +{ + @public JOYElement *_element1, *_element2, *_element3; +} +- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2 thirdElement:(JOYElement *)element2; +- (bool)updateState; +@end + static NSDictionary *CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage) { return @{ @@ -94,7 +106,7 @@ static void HIDReport(void *context, IOReturn result, void *sender, IOHIDReportT uint32_t reportID, uint8_t *report, CFIndex reportLength) { if (reportLength) { - [(__bridge JOYController *)context gotReport:[[NSData alloc] initWithBytesNoCopy:report length:reportLength freeWhenDone:NO]]; + [(__bridge JOYController *)context gotReport:[[NSData alloc] initWithBytesNoCopy:report length:reportLength freeWhenDone:false]]; } } @@ -125,9 +137,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 @@ -136,10 +176,12 @@ typedef union { NSMutableDictionary *_buttons; NSMutableDictionary *_axes; NSMutableDictionary *_axes2D; + NSMutableDictionary *_axes3D; NSMutableDictionary *_hats; NSMutableDictionary *_fullReportElements; NSMutableDictionary *> *_multiElements; - + JOYAxes3D *_lastAxes3D; + // Button emulation NSMutableDictionary *_axisEmulatedButtons; NSMutableDictionary *> *_axes2DEmulatedButtons; @@ -151,6 +193,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 +212,7 @@ typedef union { double _sentRumbleAmp; unsigned _rumbleCounter; bool _deviceCantSendReports; + dispatch_queue_t _rumbleQueue; } - (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks @@ -200,29 +247,103 @@ 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), + }; + + if (element.usagePage == kHIDPage_Sensor) { + JOYAxes3DUsage usage; + JOYElement *element1 = nil, *element2 = nil, *element3 = nil; + + switch (element.usage) { + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisX: + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisY: + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ: + usage = JOYAxes3DUsageAcceleration; + break; + case kHIDUsage_Snsr_Data_Motion_AngularPositionXAxis: + case kHIDUsage_Snsr_Data_Motion_AngularPositionYAxis: + case kHIDUsage_Snsr_Data_Motion_AngularPositionZAxis: + usage = JOYAxes3DUsageOrientation; + break; + case kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis: + usage = JOYAxes3DUsageGyroscope; + break; + default: + return; + } + + switch (element.usage) { + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisX: + case kHIDUsage_Snsr_Data_Motion_AngularPositionXAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis: + element1 = element; + if (_lastAxes3D && !_lastAxes3D->_element1 && _lastAxes3D.usage == usage) { + element2 = _lastAxes3D->_element2; + element3 = _lastAxes3D->_element3; + } + break; + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisY: + case kHIDUsage_Snsr_Data_Motion_AngularPositionYAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis: + element2 = element; + if (_lastAxes3D && !_lastAxes3D->_element2 && _lastAxes3D.usage == usage) { + element1 = _lastAxes3D->_element1; + element3 = _lastAxes3D->_element3; + } + break; + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ: + case kHIDUsage_Snsr_Data_Motion_AngularPositionZAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis: + element3 = element; + if (_lastAxes3D && !_lastAxes3D->_element3 && _lastAxes3D.usage == usage) { + element1 = _lastAxes3D->_element1; + element2 = _lastAxes3D->_element2; + } + break; + } + + _lastAxes3D = [[JOYAxes3D alloc] initWithFirstElement:element1 secondElement:element2 thirdElement:element3]; + _lastAxes3D.usage = usage; + if (element1) _axes3D[element1] = _lastAxes3D; + if (element2) _axes3D[element2] = _lastAxes3D; + if (element3) _axes3D[element3] = _lastAxes3D; + + return; + } + + 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: @@ -259,55 +380,34 @@ typedef union { if (axes2DEmulateButtons) { _axes2DEmulatedButtons[@(axes.uniqueID)] = @[ - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:axes.uniqueID | 0x100000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:axes.uniqueID | 0x200000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:axes.uniqueID | 0x300000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:axes.uniqueID | 0x400000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x100000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x200000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x300000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x400000000L], ]; } - - /* - for (NSArray *group in axes2d) { - break; - IOHIDElementRef first = (__bridge IOHIDElementRef)group[0]; - IOHIDElementRef second = (__bridge IOHIDElementRef)group[1]; - if (IOHIDElementGetUsage(first) > element.usage) continue; - if (IOHIDElementGetUsage(second) > element.usage) continue; - if (IOHIDElementGetReportID(first) != IOHIDElementGetReportID(element)) continue; - if ((IOHIDElementGetUsage(first) - kHIDUsage_GD_X) / 3 != (element.usage - kHIDUsage_GD_X) / 3) continue; - if (IOHIDElementGetParent(first) != IOHIDElementGetParent(element)) continue; - - [axes2d removeObject:group]; - [axes3d addObject:@[(__bridge id)first, (__bridge id)second, _element]]; - found = true; - break; - }*/ 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 type:JOYButtonTypeAxisEmulated 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: @@ -322,10 +422,10 @@ typedef union { [_hats setObject:hat forKey:element]; if (hatsEmulateButtons) { _hatEmulatedButtons[@(hat.uniqueID)] = @[ - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:hat.uniqueID | 0x100000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:hat.uniqueID | 0x200000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:hat.uniqueID | 0x300000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:hat.uniqueID | 0x400000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x100000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x200000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x300000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x400000000L], ]; } break; @@ -352,6 +452,7 @@ typedef union { _buttons = [NSMutableDictionary dictionary]; _axes = [NSMutableDictionary dictionary]; _axes2D = [NSMutableDictionary dictionary]; + _axes3D = [NSMutableDictionary dictionary]; _hats = [NSMutableDictionary dictionary]; _axisEmulatedButtons = [NSMutableDictionary dictionary]; _axes2DEmulatedButtons = [NSMutableDictionary dictionary]; @@ -359,11 +460,10 @@ typedef union { _iokitToJOY = [NSMutableDictionary dictionary]; - //NSMutableArray *axes3d = [NSMutableArray array]; - _hacks = hacks; _isSwitch = [_hacks[JOYIsSwitch] boolValue]; _isDualShock3 = [_hacks[JOYIsDualShock3] boolValue]; + _isSony = [_hacks[JOYIsSony] boolValue]; NSDictionary *customReports = hacks[JOYCustomReports]; _lastReport = [NSMutableData dataWithLength:MAX( @@ -416,8 +516,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) { @@ -477,6 +577,19 @@ typedef union { if (_isSwitch) { [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x04} length:2]]; [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x02} length:2]]; + + _lastVendorSpecificOutput.switchPacket.reportID = 0x1; // Rumble and LEDs + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 3; // Set input report mode + _lastVendorSpecificOutput.switchPacket.commandData[0] = 0x30; // Standard full mode + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 0x40; // Enable/disableIMU + _lastVendorSpecificOutput.switchPacket.commandData[0] = 1; // Enabled + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; } if (_isDualShock3) { @@ -490,8 +603,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; } @@ -546,6 +687,11 @@ typedef union { return [[NSSet setWithArray:[_axes2D allValues]] allObjects]; } +- (NSArray *)axes3D +{ + return [[NSSet setWithArray:[_axes3D allValues]] allObjects]; +} + - (NSArray *)hats { return [_hats allValues]; @@ -564,7 +710,9 @@ typedef union { } } } - [self updateRumble]; + dispatch_async(_rumbleQueue, ^{ + [self updateRumble]; + }); } - (void)elementChanged:(IOHIDElementRef)element @@ -661,6 +809,20 @@ typedef union { } } + { + JOYAxes3D *axes = _axes3D[element]; + if (axes) { + if ([axes updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedAxes3D:)]) { + [listener controller:self movedAxes3D:axes]; + } + } + } + return; + } + } + { JOYHat *hat = _hats[element]; if (hat) { @@ -699,7 +861,9 @@ typedef union { _physicallyConnected = false; [exposedControllers removeObject:self]; [self setRumbleAmplitude:0]; - [self updateRumble]; + dispatch_sync(_rumbleQueue, ^{ + [self updateRumble]; + }); _device = nil; } @@ -716,9 +880,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 (_isDualSense) { + 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 +975,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 +994,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 +1053,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 +1127,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/JoyKit/JOYEmulatedButton.h b/JoyKit/JOYEmulatedButton.h index 491e0c7..05ccde8 100644 --- a/JoyKit/JOYEmulatedButton.h +++ b/JoyKit/JOYEmulatedButton.h @@ -4,7 +4,7 @@ #import "JOYHat.h" @interface JOYEmulatedButton : JOYButton -- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +- (instancetype)initWithUsage:(JOYButtonUsage)usage type:(JOYButtonType)type uniqueID:(uint64_t)uniqueID; - (bool)updateStateFromAxis:(JOYAxis *)axis; - (bool)updateStateFromAxes2D:(JOYAxes2D *)axes; - (bool)updateStateFromHat:(JOYHat *)hat; diff --git a/JoyKit/JOYEmulatedButton.m b/JoyKit/JOYEmulatedButton.m index 1ebed3a..5e6d1b3 100644 --- a/JoyKit/JOYEmulatedButton.m +++ b/JoyKit/JOYEmulatedButton.m @@ -1,4 +1,5 @@ #import "JOYEmulatedButton.h" +#import @interface JOYButton () { @@ -9,13 +10,15 @@ @implementation JOYEmulatedButton { uint64_t _uniqueID; + JOYButtonType _type; } -- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +- (instancetype)initWithUsage:(JOYButtonUsage)usage type:(JOYButtonType)type uniqueID:(uint64_t)uniqueID; { self = [super init]; self.usage = usage; _uniqueID = uniqueID; + _type = type; return self; } @@ -28,7 +31,7 @@ - (bool)updateStateFromAxis:(JOYAxis *)axis { bool old = _state; - _state = [axis value] > 0.5; + _state = [axis value] > 0.8; return _state != old; } @@ -88,4 +91,9 @@ return _state != old; } +- (JOYButtonType)type +{ + return _type; +} + @end diff --git a/JoyKit/JOYFullReportElement.m b/JoyKit/JOYFullReportElement.m index c8efb27..a19a530 100644 --- a/JoyKit/JOYFullReportElement.m +++ b/JoyKit/JOYFullReportElement.m @@ -61,9 +61,10 @@ return self.uniqueID; } -- (BOOL)isEqual:(id)object +- (BOOL)isEqual:(JOYFullReportElement *)object { - return self.uniqueID == self.uniqueID; + if ([object isKindOfClass:self.class]) return false; + return self.uniqueID == object.uniqueID; } - (id)copyWithZone:(nullable NSZone *)zone; diff --git a/JoyKit/JOYHat.m b/JoyKit/JOYHat.m index 743e49c..b5a18f0 100644 --- a/JoyKit/JOYHat.m +++ b/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/JoyKit/JOYSubElement.m b/JoyKit/JOYSubElement.m index c94badc..186caf9 100644 --- a/JoyKit/JOYSubElement.m +++ b/JoyKit/JOYSubElement.m @@ -57,6 +57,12 @@ memcpy(temp, bytes + _offset / 8, (_offset + _size - 1) / 8 - _offset / 8 + 1); uint32_t ret = (*(uint32_t *)temp) >> (_offset % 8); ret &= (1 << _size) - 1; + // + if (_min < 0 || _max < 0) { // Uses unsigned values + if (ret & (1 << (_size - 1)) ) { // Is negative + ret |= ~((1 << _size) - 1); // Fill with 1s + } + } if (_max < _min) { return _max + _min - ret; diff --git a/LICENSE b/LICENSE index 17619e9..3303e0d 100644 --- a/LICENSE +++ b/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/Makefile b/Makefile index 22dccf1..51f7abb 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ endif ifeq ($(PLATFORM),windows32) _ := $(shell chcp 65001) EXESUFFIX:=.exe -NATIVE_CC = clang -IWindows -Wno-deprecated-declarations +NATIVE_CC = clang -IWindows -Wno-deprecated-declarations --target=i386-pc-windows else EXESUFFIX:= NATIVE_CC := cc @@ -30,13 +30,21 @@ 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),) MAKECMDGOALS := $(DEFAULT) endif -VERSION := 0.13.6 +include version.mk export VERSION CONF ?= debug SDL_AUDIO_DRIVER ?= sdl @@ -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,14 +121,17 @@ 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) -SDL_LDFLAGS := $(shell sdl2-config --libs) +SDL_LDFLAGS := $(shell sdl2-config --libs) -lpthread else SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2) -SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) +SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) -lpthread endif ifeq (,$(PKG_CONFIG)) GL_LDFLAGS := -lGL @@ -129,8 +140,8 @@ GL_CFLAGS := $(shell $(PKG_CONFIG) --cflags gl) GL_LDFLAGS := $(shell $(PKG_CONFIG) --libs gl || echo -lGL) endif ifeq ($(PLATFORM),windows32) -CFLAGS += -IWindows -Drandom=rand -LDFLAGS += -lmsvcrt -lcomdlg32 -luser32 -lSDL2main -Wl,/MANIFESTFILE:NUL +CFLAGS += -IWindows -Drandom=rand --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 @@ -164,13 +175,14 @@ CFLAGS += -O3 -DNDEBUG STRIP := strip ifeq ($(PLATFORM),Darwin) LDFLAGS += -Wl,-exported_symbols_list,$(NULL) -STRIP := -@true +STRIP := strip -x +endif +ifeq ($(PLATFORM),windows32) +LDFLAGS += -fuse-ld=lld endif -ifneq ($(PLATFORM),windows32) LDFLAGS += -flto CFLAGS += -flto LDFLAGS += -Wno-lto-type-mismatch # For GCC's LTO -endif else $(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release") @@ -188,8 +200,8 @@ endif cocoa: $(BIN)/SameBoy.app quicklook: $(BIN)/SameBoy.qlgenerator -sdl: $(SDL_TARGET) $(BIN)/SDL/dmg_boot.bin $(BIN)/SDL/cgb_boot.bin $(BIN)/SDL/agb_boot.bin $(BIN)/SDL/sgb_boot.bin $(BIN)/SDL/sgb2_boot.bin $(BIN)/SDL/LICENSE $(BIN)/SDL/registers.sym $(BIN)/SDL/background.bmp $(BIN)/SDL/Shaders -bootroms: $(BIN)/BootROMs/agb_boot.bin $(BIN)/BootROMs/cgb_boot.bin $(BIN)/BootROMs/dmg_boot.bin $(BIN)/BootROMs/sgb_boot.bin $(BIN)/BootROMs/sgb2_boot.bin +sdl: $(SDL_TARGET) $(BIN)/SDL/dmg_boot.bin $(BIN)/SDL/mgb_boot.bin $(BIN)/SDL/cgb0_boot.bin $(BIN)/SDL/cgb_boot.bin $(BIN)/SDL/agb_boot.bin $(BIN)/SDL/sgb_boot.bin $(BIN)/SDL/sgb2_boot.bin $(BIN)/SDL/LICENSE $(BIN)/SDL/registers.sym $(BIN)/SDL/background.bmp $(BIN)/SDL/Shaders +bootroms: $(BIN)/BootROMs/agb_boot.bin $(BIN)/BootROMs/cgb_boot.bin $(BIN)/BootROMs/cgb0_boot.bin $(BIN)/BootROMs/dmg_boot.bin $(BIN)/BootROMs/sgb_boot.bin $(BIN)/BootROMs/sgb2_boot.bin tester: $(TESTER_TARGET) $(BIN)/tester/dmg_boot.bin $(BIN)/tester/cgb_boot.bin $(BIN)/tester/agb_boot.bin $(BIN)/tester/sgb_boot.bin $(BIN)/tester/sgb2_boot.bin all: cocoa sdl tester libretro wasm @@ -268,6 +280,8 @@ $(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \ Cocoa/Info.plist \ Misc/registers.sym \ $(BIN)/SameBoy.app/Contents/Resources/dmg_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/mgb_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/cgb0_boot.bin \ $(BIN)/SameBoy.app/Contents/Resources/cgb_boot.bin \ $(BIN)/SameBoy.app/Contents/Resources/agb_boot.bin \ $(BIN)/SameBoy.app/Contents/Resources/sgb_boot.bin \ @@ -406,6 +420,7 @@ $(OBJ)/BootROMs/SameBoyLogo.pb12: $(OBJ)/BootROMs/SameBoyLogo.2bpp $(PB12_COMPRE $(PB12_COMPRESS): BootROMs/pb12.c $(NATIVE_CC) -std=c99 -Wall -Werror $< -o $@ +$(BIN)/BootROMs/cgb0_boot.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/agb_boot.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/cgb_boot_fast.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/sgb2_boot: BootROMs/sgb_boot.asm @@ -425,6 +440,49 @@ libretro: wasm: $(MAKE) -C wasm +# 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/Misc/registers.sym b/Misc/registers.sym index 3b31b74..affe663 100644 --- a/Misc/registers.sym +++ b/Misc/registers.sym @@ -57,11 +57,10 @@ 00:FF6B IO_OBPD 00:FF6C IO_OPRI 00:FF70 IO_SVBK -00:FF72 IO_UNKNOWN2 -00:FF73 IO_UNKNOWN3 -00:FF74 IO_UNKNOWN4 +00:FF72 IO_PSWX +00:FF73 IO_PSWY +00:FF74 IO_PSW 00:FF75 IO_UNKNOWN5 -00:FF76 IO_PCM_12 -00:FF77 IO_PCM_34 -00:FF7F IO_UNKNOWN8 +00:FF76 IO_PCM12 +00:FF77 IO_PCM34 00:FFFF IO_IE diff --git a/OpenDialog/cocoa.m b/OpenDialog/cocoa.m index 76b9606..cfb2553 100644 --- a/OpenDialog/cocoa.m +++ b/OpenDialog/cocoa.m @@ -5,6 +5,8 @@ char *do_open_rom_dialog(void) { @autoreleasepool { + int stderr_fd = dup(STDERR_FILENO); + close(STDERR_FILENO); NSWindow *key = [NSApp keyWindow]; NSOpenPanel *dialog = [NSOpenPanel openPanel]; dialog.title = @"Open ROM"; @@ -12,6 +14,28 @@ char *do_open_rom_dialog(void) [dialog runModal]; [key makeKeyAndOrderFront:nil]; NSString *ret = [[[dialog URLs] firstObject] path]; + dup2(stderr_fd, STDERR_FILENO); + if (ret) { + return strdup(ret.UTF8String); + } + return NULL; + } +} + +char *do_open_folder_dialog(void) +{ + @autoreleasepool { + int stderr_fd = dup(STDERR_FILENO); + close(STDERR_FILENO); + 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]; + dup2(stderr_fd, STDERR_FILENO); if (ret) { return strdup(ret.UTF8String); } diff --git a/OpenDialog/gtk.c b/OpenDialog/gtk.c index 5b1caa3..378dcb4 100644 --- a/OpenDialog/gtk.c +++ b/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/OpenDialog/open_dialog.h b/OpenDialog/open_dialog.h index 85e5721..6d7fb5b 100644 --- a/OpenDialog/open_dialog.h +++ b/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/OpenDialog/windows.c b/OpenDialog/windows.c index 52e281d..e711032 100644 --- a/OpenDialog/windows.c +++ b/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/QuickLook/Info.plist b/QuickLook/Info.plist index b01aae1..9b369ec 100644 --- a/QuickLook/Info.plist +++ b/QuickLook/Info.plist @@ -48,7 +48,7 @@ CFPlugInUnloadFunction NSHumanReadableCopyright - Copyright © 2015-2020 Lior Halphon + Copyright © 2015-2021 Lior Halphon QLNeedsToBeRunInMainThread QLPreviewHeight diff --git a/QuickLook/exports.sym b/QuickLook/exports.sym index f979687..2e7fdde 100644 --- a/QuickLook/exports.sym +++ b/QuickLook/exports.sym @@ -1,3 +1 @@ -_DeallocQuickLookGeneratorPluginType -_QuickLookGeneratorQueryInterface _QuickLookGeneratorPluginFactory diff --git a/QuickLook/generator.m b/QuickLook/generator.m index 92bb6ac..f2651d2 100644 --- a/QuickLook/generator.m +++ b/QuickLook/generator.m @@ -43,10 +43,10 @@ static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) bitmapInfo, provider, NULL, - YES, + true, renderingIntent); CGContextSetInterpolationQuality(cgContext, kCGInterpolationNone); - NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithGraphicsPort:(void *)cgContext flipped:NO]; + NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithGraphicsPort:(void *)cgContext flipped:false]; [NSGraphicsContext setCurrentContext:context]; diff --git a/QuickLook/main.c b/QuickLook/main.c index 1d1676a..4e45313 100644 --- a/QuickLook/main.c +++ b/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/README.md b/README.md index 88b8caa..d06ef4d 100644 --- a/README.md +++ b/README.md @@ -33,21 +33,26 @@ Features currently supported only with the Cocoa version: ## Compatibility SameBoy passes all of [blargg's test ROMs](http://gbdev.gg8.se/wiki/articles/Test_ROMs#Blargg.27s_tests), all of [mooneye-gb's](https://github.com/Gekkio/mooneye-gb) tests (Some tests require the original boot ROMs), and all of [Wilbert Pol's tests](https://github.com/wilbertpol/mooneye-gb/tree/master/tests/acceptance). SameBoy should work with most games and demos, please [report](https://github.com/LIJI32/SameBoy/issues/new) any broken ROM. The latest results for SameBoy's automatic tester are available [here](https://sameboy.github.io/automation/). +## Contributing +SameBoy is an open-source project licensed under the MIT license, and you're welcome to contribute by creating issues, implementing new features, improving emulation accuracy and fixing existing open issues. You can read the [contribution guidelines](CONTRIBUTING.md) to make sure your contributions are as effective as possible. + ## Compilation SameBoy requires the following tools and libraries to build: - * clang + * clang (Recommended; required for macOS) or GCC * make - * Cocoa port: OS X SDK and Xcode command line tools + * 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.) * [GnuWin](http://gnuwin32.sourceforge.net/) - * Running vcvars32 before running make. Make sure all required tools and libraries are in %PATH% and %lib%, respectively. + * Running vcvars32 before running make. Make sure all required tools and libraries are in %PATH% and %lib%, respectively. (see [Build FAQ](https://github.com/LIJI32/SameBoy/blob/master/build-faq.md) for more details on Windows compilation) 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/SDL/console.c b/SDL/console.c new file mode 100644 index 0000000..ad9c2b5 --- /dev/null +++ b/SDL/console.c @@ -0,0 +1,1020 @@ +#include "console.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ESC(x) "\x1B" x +#define CSI(x) ESC("[" x) +#define SGR(x) CSI(x "m") + +static bool initialized = false; +typedef struct listent_s listent_t; + +struct listent_s { + listent_t *prev; + listent_t *next; + char content[]; +}; + +typedef struct { + listent_t *first; + listent_t *last; +} fifo_t; + +static fifo_t lines; +static fifo_t history; + +static void remove_entry(fifo_t *fifo, listent_t *entry) +{ + if (fifo->last == entry) { + fifo->last = entry->prev; + } + if (fifo->first == entry) { + fifo->first = entry->next; + } + if (entry->next) { + entry->next->prev = entry->prev; + } + if (entry->prev) { + entry->prev->next = entry->next; + } + free(entry); +} + +static void add_entry(fifo_t *fifo, const char *content) +{ + size_t length = strlen(content); + listent_t *entry = malloc(sizeof(*entry) + length + 1); + entry->next = NULL; + entry->prev = fifo->last; + memcpy(entry->content, content, length); + entry->content[length] = 0; + if (fifo->last) { + fifo->last->next = entry; + } + fifo->last = entry; + if (!fifo->first) { + fifo->first = entry; + } +} + +static listent_t *reverse_find(listent_t *entry, const char *string, bool exact) +{ + while (entry) { + if (exact && strcmp(entry->content, string) == 0) { + return entry; + } + if (!exact && strstr(entry->content, string)) { + return entry; + } + entry = entry->prev; + } + return NULL; +} + +static bool is_term(void) +{ + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) return false; +#ifdef _WIN32 + if (AllocConsole()) { + FreeConsole(); + return false; + } + + unsigned long input_mode, output_mode; + + GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &input_mode); + GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &output_mode); + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_INPUT); + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + CONSOLE_SCREEN_BUFFER_INFO before = {0,}; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &before); + + printf(SGR("0")); + + CONSOLE_SCREEN_BUFFER_INFO after = {0,}; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &after); + + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), input_mode); + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), output_mode); + + + if (before.dwCursorPosition.X != after.dwCursorPosition.X || + before.dwCursorPosition.Y != after.dwCursorPosition.Y) { + printf("\r \r"); + return false; + } + return true; +#else + return getenv("TERM"); +#endif +} + +static unsigned width, height; + +static char raw_getc(void) +{ +#ifdef _WIN32 + char c; + unsigned long ret; + ReadConsole(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &ret, NULL); +#else + ssize_t ret; + char c; + + do { + ret = read(STDIN_FILENO, &c, 1); + } while (ret == -1 && errno == EINTR); +#endif + return ret == 1? c : EOF; +} + +#ifdef _WIN32 +#pragma clang diagnostic ignored "-Wmacro-redefined" +#include + +static void update_size(void) +{ + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + width = csbi.srWindow.Right - csbi.srWindow.Left + 1; + height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; +} + +static unsigned long input_mode, output_mode; + +static void cleanup(void) +{ + printf(CSI("!p")); // reset + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), input_mode); + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), output_mode); + fflush(stdout); +} + +static bool initialize(void) +{ + if (!is_term()) return false; + update_size(); + if (width == 0 || height == 0) { + return false; + } + + static bool once = false; + if (!once) { + atexit(cleanup); + GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &input_mode); + GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &output_mode); + once = true; + } + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_INPUT); + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + printf(CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height); + + fflush(stdout); + initialized = true; + return true; +} +#else +#include + +static void update_size(void) +{ + struct winsize winsize; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize); + width = winsize.ws_col; + height = winsize.ws_row; +} + +static void terminal_resized(int ignored) +{ + update_size(); +} + +#include +static struct termios terminal; + + +static void cleanup(void) +{ + printf(CSI("!p")); // reset + tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal); + fflush(stdout); +} + +static bool initialize(void) +{ + if (!is_term()) return false; + update_size(); + if (width == 0 || height == 0) { + return false; + } + + static bool once = false; + if (!once) { + atexit(cleanup); + signal(SIGWINCH, terminal_resized); + tcgetattr(STDIN_FILENO, &terminal); +#ifdef _WIN32 + _setmode(STDIN_FILENO, _O_TEXT); +#endif + once = true; + } + struct termios raw_terminal; + raw_terminal = terminal; + raw_terminal.c_lflag = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_terminal); + + printf(CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height); + + fflush(stdout); + initialized = true; + return true; +} +#endif + +static struct { + char *content; + size_t allocation_size; + size_t length; + size_t position; + size_t scroll; + bool reverse_search; + listent_t *search_line; +} line; + +#define CTL(x) ((x) - 'A' + 1) + +static const char *prompt = ""; +static size_t prompt_length = 0; +static bool repeat_empty = false; + +static bool redraw_prompt(bool force) +{ + if (line.reverse_search) { + if (!force) return false; + if (line.length == 0) { + printf("\r" CSI("K") "%s" SGR("2") "Reverse Search..." SGR("0") CSI("%zuG"), prompt, prompt_length + 1); + return true; + } + if (!line.search_line) { + printf("\r" CSI("K") "%s" SGR("1") "%s" SGR("0"), prompt, line.content); + return true; + } + const char *loc = strstr(line.search_line->content, line.content); + printf("\r" CSI("K") "%s" "%.*s" SGR("1") "%s" SGR("0") "%s" CSI("%uG"), + prompt, + (int)(loc - line.search_line->content), + line.search_line->content, + line.content, + loc + line.length, + (unsigned)(loc - line.search_line->content + line.length + prompt_length + 1)); + return true; + } + + size_t max = width - 1 - prompt_length; + + if (line.scroll && line.length <= max) { + line.scroll = 0; + force = true; + } + + if (line.scroll > line.length - max) { + line.scroll = line.length - max; + force = true; + } + + if (line.position < line.scroll + 1 && line.position) { + line.scroll = line.position - 1; + force = true; + } + + if (line.position == 0 && line.scroll) { + line.scroll = 0; + force = true; + } + + if (line.position > line.scroll + max) { + line.scroll = line.position - max; + force = true; + } + + if (!force && line.length <= max) { + return false; + } + + if (line.length <= max) { + printf("\r" CSI("K") "%s%s" CSI("%uG"), prompt, line.content, (unsigned)(line.position + prompt_length + 1)); + return true; + } + + size_t left = max; + const char *string = line.content + line.scroll; + printf("\r" CSI("K") "%s", prompt); + if (line.scroll) { + printf(SGR("2") "%c" SGR("0"), *string); + string++; + left--; + } + if (line.scroll + max == line.length) { + printf("%s", string); + } + else { + printf("%.*s", (int)(left - 1), string); + string += left; + left = 1; + printf(SGR("2") "%c" SGR("0"), *string); + } + printf(CSI("%uG"), (unsigned)(line.position - line.scroll + prompt_length + 1)); + + return true; +} + +static void set_position(size_t position) +{ + if (position > line.length) { + printf("\a"); + return; + } + line.position = position; + if (!redraw_prompt(false)) { + printf(CSI("%uG"), (unsigned)(position + prompt_length + 1)); + } +} + +static void set_line(const char *content) +{ + line.length = strlen(content); + if (line.length + 1 > line.allocation_size) { + line.content = realloc(line.content, line.length + 1); + line.allocation_size = line.length + 1; + } + else if (line.allocation_size > 256 && line.length < 128) { + line.content = realloc(line.content, line.length + 1); + line.allocation_size = line.length + 1; + } + line.position = line.length; + strcpy(line.content, content); + redraw_prompt(true); +} + +static void insert(const char *string) +{ + size_t insertion_length = strlen(string); + size_t new_length = insertion_length + line.length; + bool need_realloc = false; + while (line.allocation_size < new_length + 1) { + line.allocation_size *= 2; + need_realloc = true; + } + if (need_realloc) { + line.content = realloc(line.content, line.allocation_size); + } + memmove(line.content + line.position + insertion_length, + line.content + line.position, + line.length - line.position); + memcpy(line.content + line.position, string, insertion_length); + line.position += insertion_length; + line.content[new_length] = 0; + line.length = new_length; + if (!redraw_prompt(line.position != line.length)) { + printf("%s", string); + } +} + +static void delete(size_t size, bool forward) +{ + if (line.length < size) { + printf("\a"); + return; + } + if (forward) { + if (line.position > line.length - size) { + printf("\a"); + return; + } + else { + line.position += size; + } + } + else if (line.position < size) { + printf("\a"); + return; + } + memmove(line.content + line.position - size, + line.content + line.position, + line.length - line.position); + line.length -= size; + line.content[line.length] = 0; + line.position -= size; + + if (!redraw_prompt(line.position != line.length)) { + printf(CSI("%uG") CSI("K"), + (unsigned)(line.position + prompt_length + 1)); + } +} + +static void move_word(bool forward) +{ + signed offset = forward? 1 : -1; + size_t end = forward? line.length : 0; + signed check_offset = forward? 0 : -1; + if (line.position == end) { + printf("\a"); + return; + } + line.position += offset; + while (line.position != end && isalnum(line.content[line.position + check_offset])) { + line.position += offset; + } + if (!redraw_prompt(false)) { + printf(CSI("%uG"), (unsigned)(line.position + prompt_length + 1)); + } +} + +static void delete_word(bool forward) +{ + size_t original_pos = line.position; + signed offset = forward? 1 : -1; + size_t end = forward? line.length : 0; + signed check_offset = forward? 0 : -1; + if (line.position == end) { + printf("\a"); + return; + } + line.position += offset; + while (line.position != end && isalnum(line.content[line.position + check_offset])) { + line.position += offset; + } + if (forward) { + delete(line.position - original_pos, false); + } + else { + delete(original_pos - line.position, true); + } +} + +#define MOD_ALT(x) (0x100 | x) +#define MOD_SHIFT(x) (0x200 | x) +#define MOD_CTRL(x) (0x400 | x) +#define MOD_SPECIAL(x) (0x800 | x) + +static unsigned get_extended_key(void) +{ + unsigned modifiers = 0; + char c = 0; +restart: + c = raw_getc(); + if (c == 0x1B) { + modifiers = MOD_SHIFT(MOD_ALT(0)); + goto restart; + } + else if (c != '[' && c != 'O') { + return MOD_ALT(c); + } + unsigned ret = 0; + while (true) { + c = raw_getc(); + if (c >= '0' && c <= '9') { + ret = ret * 10 + c - '0'; + } + else if (c == ';') { + if (ret == 1) { + modifiers |= MOD_ALT(0); + } + else if (ret == 2) { + modifiers |= MOD_SHIFT(0); + } + else if (ret == 5) { + modifiers |= MOD_CTRL(0); + } + ret = 0; + } + else if (c == '~') { + return MOD_SPECIAL(ret) | modifiers; + } + else { + if (ret == 1) { + modifiers |= MOD_ALT(0); + } + else if (ret == 2) { + modifiers |= MOD_SHIFT(0); + } + else if (ret == 5) { + modifiers |= MOD_CTRL(0); + } + return c | modifiers; + } + } +} + +#define SWAP(x, y) do {typeof(*(x)) _tmp = *(x); *(x) = *(y);*(y) = _tmp;} while (0) +static pthread_mutex_t terminal_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t lines_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t lines_cond = PTHREAD_COND_INITIALIZER; + +static char reverse_search_mainloop(void) +{ + while (true) { + char c = raw_getc(); + pthread_mutex_lock(&terminal_lock); + + switch (c) { + case CTL('C'): + line.search_line = NULL; + set_line(""); + pthread_mutex_unlock(&terminal_lock); + return CTL('A'); + case CTL('R'): + line.search_line = reverse_find(line.search_line? line.search_line->prev : history.last, line.content, false); + if (!line.search_line) { + printf("\a"); + } + redraw_prompt(true); + break; + case CTL('W'): + delete_word(false); + redraw_prompt(true); + break; +#ifndef _WIN32 + case CTL('Z'): + set_line(""); + raise(SIGSTOP); + initialize(); // Reinitialize + redraw_prompt(true); + break; +#endif + case CTL('H'): + case 0x7F: // Backspace + delete(1, false); + redraw_prompt(true); + break; + default: + if (c >= ' ') { + char string[2] = {c, 0}; + insert(string); + line.search_line = reverse_find(line.search_line?: history.last, line.content, false); + if (!line.search_line) { + printf("\a"); + } + redraw_prompt(true); + } + else { + pthread_mutex_unlock(&terminal_lock); + return c; + } + break; + } + pthread_mutex_unlock(&terminal_lock); + fflush(stdout); + } + +} + + +static +#ifdef _WIN32 +int __stdcall +#else +void * +#endif +mainloop(char *(*completer)(const char *substring, uintptr_t *context)) +{ + listent_t *history_line = NULL; + uintptr_t complete_context = 0; + size_t completion_length = 0; + while (true) { + char c; + if (line.reverse_search) { + c = reverse_search_mainloop(); + line.reverse_search = false; + if (line.search_line) { + size_t pos = strstr(line.search_line->content, line.content) - line.search_line->content + line.length; + set_line(line.search_line->content); + line.search_line = NULL; + set_position(pos); + } + else { + redraw_prompt(true); + } + } + else { + c = raw_getc(); + } + pthread_mutex_lock(&terminal_lock); + + switch (c) { + case CTL('A'): + set_position(0); + complete_context = completion_length = 0; + break; + case CTL('B'): + set_position(line.position - 1); + complete_context = completion_length = 0; + break; + case CTL('C'): + if (line.length) { + set_line(""); + history_line = NULL; + complete_context = completion_length = 0; + } + else { +#ifdef _WIN32 + raise(SIGINT); +#else + kill(getpid(), SIGINT); +#endif + } + break; + case CTL('D'): + if (line.length) { + delete(1, true); + complete_context = completion_length = 0; + } + else { + pthread_mutex_lock(&lines_lock); + add_entry(&lines, CON_EOF); + pthread_cond_signal(&lines_cond); + pthread_mutex_unlock(&lines_lock); + } + break; + case CTL('E'): + set_position(line.length); + complete_context = completion_length = 0; + break; + case CTL('F'): + set_position(line.position + 1); + complete_context = completion_length = 0; + break; + case CTL('K'): + printf(CSI("K")); + if (!redraw_prompt(false)) { + line.length = line.position; + line.content[line.length] = 0; + } + complete_context = completion_length = 0; + break; + case CTL('R'): + complete_context = completion_length = 0; + line.reverse_search = true; + set_line(""); + + break; + case CTL('T'): + if (line.length < 2) { + printf("\a"); + break; + } + if (line.position && line.position == line.length) { + line.position--; + } + if (line.position == 0) { + printf("\a"); + break; + } + SWAP(line.content + line.position, + line.content + line.position - 1); + line.position++; + redraw_prompt(true); + complete_context = completion_length = 0; + break; + case CTL('W'): + delete_word(false); + complete_context = completion_length = 0; + break; +#ifndef _WIN32 + case CTL('Z'): + set_line(""); + complete_context = completion_length = 0; + raise(SIGSTOP); + initialize(); // Reinitialize + break; +#endif + case '\r': + case '\n': + pthread_mutex_lock(&lines_lock); + if (line.length == 0 && repeat_empty && history.last) { + add_entry(&lines, history.last->content); + } + else { + add_entry(&lines, line.content); + } + pthread_cond_signal(&lines_cond); + pthread_mutex_unlock(&lines_lock); + if (line.length) { + listent_t *dup = reverse_find(history.last, line.content, true); + if (dup) { + remove_entry(&history, dup); + } + add_entry(&history, line.content); + set_line(""); + history_line = NULL; + } + complete_context = completion_length = 0; + break; + case CTL('H'): + case 0x7F: // Backspace + delete(1, false); + complete_context = completion_length = 0; + break; + case 0x1B: + switch (get_extended_key()) { + case MOD_SPECIAL(1): // Home + case MOD_SPECIAL(7): + case 'H': + set_position(0); + complete_context = completion_length = 0; + break; + case MOD_SPECIAL(8): // End + case 'F': + set_position(line.length); + complete_context = completion_length = 0; + break; + case MOD_SPECIAL(3): // Delete + delete(1, true); + complete_context = completion_length = 0; + break; + case 'A': // Up + if (!history_line) { + history_line = history.last; + } + else { + history_line = history_line->prev; + } + if (history_line) { + set_line(history_line->content); + complete_context = completion_length = 0; + } + else { + history_line = history.first; + printf("\a"); + } + + break; + case 'B': // Down + if (!history_line) { + printf("\a"); + break; + } + history_line = history_line->next; + if (history_line) { + set_line(history_line->content); + complete_context = completion_length = 0; + } + else { + set_line(""); + complete_context = completion_length = 0; + } + break; + case 'C': // Right + set_position(line.position + 1); + complete_context = completion_length = 0; + break; + case 'D': // Left + set_position(line.position - 1); + complete_context = completion_length = 0; + break; + case MOD_ALT('b'): + case MOD_ALT('D'): + move_word(false); + complete_context = completion_length = 0; + break; + case MOD_ALT('f'): + case MOD_ALT('C'): + move_word(true); + complete_context = completion_length = 0; + break; + case MOD_ALT(0x7f): // ALT+Backspace + delete_word(false); + complete_context = completion_length = 0; + break; + case MOD_ALT('('): // ALT+Delete + delete_word(true); + complete_context = completion_length = 0; + break; + default: + printf("\a"); + break; + } + break; + case '\t': { + char temp = line.content[line.position - completion_length]; + line.content[line.position - completion_length] = 0; + char *completion = completer? completer(line.content, &complete_context) : NULL; + line.content[line.position - completion_length] = temp; + if (completion) { + if (completion_length) { + delete(completion_length, false); + } + insert(completion); + completion_length = strlen(completion); + free(completion); + } + else { + printf("\a"); + } + break; + } + default: + if (c >= ' ') { + char string[2] = {c, 0}; + insert(string); + complete_context = completion_length = 0; + } + else { + printf("\a"); + } + break; + } + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); + } + return 0; +} + +char *CON_readline(const char *new_prompt) +{ + pthread_mutex_lock(&terminal_lock); + const char *old_prompt = prompt; + prompt = new_prompt; + prompt_length = strlen(prompt); + redraw_prompt(true); + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); + + pthread_mutex_lock(&lines_lock); + while (!lines.first) { + pthread_cond_wait(&lines_cond, &lines_lock); + } + char *ret = strdup(lines.first->content); + remove_entry(&lines, lines.first); + pthread_mutex_unlock(&lines_lock); + + pthread_mutex_lock(&terminal_lock); + prompt = old_prompt; + prompt_length = strlen(prompt); + redraw_prompt(true); + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); + return ret; +} + +char *CON_readline_async(void) +{ + char *ret = NULL; + pthread_mutex_lock(&lines_lock); + if (lines.first) { + ret = strdup(lines.first->content); + remove_entry(&lines, lines.first); + } + pthread_mutex_unlock(&lines_lock); + return ret; +} + +bool CON_start(char *(*completer)(const char *substring, uintptr_t *context)) +{ + if (!initialize()) { + return false; + } + set_line(""); + pthread_t thread; + return pthread_create(&thread, NULL, (void *)mainloop, completer) == 0; +} + +void CON_attributed_print(const char *string, CON_attributes_t *attributes) +{ + if (!initialized) { + printf("%s", string); + return; + } + static bool pending_newline = false; + pthread_mutex_lock(&terminal_lock); + printf(ESC("8")); + bool needs_reset = false; + if (attributes) { + if (attributes->color) { + if (attributes->color >= 0x10) { + printf(SGR("%d"), attributes->color - 0x11 + 90); + } + else { + printf(SGR("%d"), attributes->color - 1 + 30); + } + needs_reset = true; + } + if (attributes->background) { + if (attributes->background >= 0x10) { + printf(SGR("%d"), attributes->background - 0x11 + 100); + } + else { + printf(SGR("%d"), attributes->background - 1 + 40); + } + needs_reset = true; + } + if (attributes->bold) { + printf(SGR("1")); + needs_reset = true; + } + if (attributes->italic) { + printf(SGR("3")); + needs_reset = true; + } + if (attributes->underline) { + printf(SGR("4")); + needs_reset = true; + } + } + const char *it = string; + bool need_redraw_prompt = false; + while (*it) { + if (pending_newline) { + need_redraw_prompt = true; + printf("\n" CSI("K") "\n" CSI("A")); + pending_newline = false; + continue; + } + if (*it == '\n') { + printf("%.*s", (int)(it - string), string); + string = it + 1; + pending_newline = true; + } + it++; + } + if (*string) { + printf("%s", string); + } + if (needs_reset) { + printf(SGR("0")); + } + printf(ESC("7") CSI("B")); + if (need_redraw_prompt) { + redraw_prompt(true); + } + else { + set_position(line.position); + } + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); +} + +void CON_print(const char *string) +{ + CON_attributed_print(string, NULL); +} + +void CON_vprintf(const char *fmt, va_list args) +{ + char *string = NULL; + vasprintf(&string, fmt, args); + CON_attributed_print(string, NULL); + free(string); +} + +void CON_attributed_vprintf(const char *fmt, CON_attributes_t *attributes, va_list args) +{ + char *string = NULL; + vasprintf(&string, fmt, args); + CON_attributed_print(string, attributes); + free(string); +} + +void CON_printf(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + CON_vprintf(fmt, args); + va_end(args); +} + + +void CON_attributed_printf(const char *fmt, CON_attributes_t *attributes,...) +{ + va_list args; + va_start(args, attributes); + CON_attributed_vprintf(fmt, attributes, args); + va_end(args); +} + +void CON_set_async_prompt(const char *string) +{ + pthread_mutex_lock(&terminal_lock); + prompt = string; + prompt_length = strlen(string); + redraw_prompt(true); + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); +} + +void CON_set_repeat_empty(bool repeat) +{ + repeat_empty = repeat; +} diff --git a/SDL/console.h b/SDL/console.h new file mode 100644 index 0000000..d158988 --- /dev/null +++ b/SDL/console.h @@ -0,0 +1,49 @@ +#include +#include +#include + +#define CON_EOF "\x04" +bool CON_start(char *(*completer)(const char *substring, uintptr_t *context)); +char *CON_readline(const char *prompt); +char *CON_readline_async(void); + +typedef struct { + enum { + CON_COLOR_NONE = 0, + CON_COLOR_BLACK, + CON_COLOR_RED, + CON_COLOR_GREEN, + CON_COLOR_YELLOW, + CON_COLOR_BLUE, + CON_COLOR_MAGENTA, + CON_COLOR_CYAN, + CON_COLOR_LIGHT_GREY, + + CON_COLOR_DARK_GREY = CON_COLOR_BLACK + 0x10, + CON_COLOR_BRIGHT_RED = CON_COLOR_RED + 0x10, + CON_COLOR_BRIGHT_GREEN = CON_COLOR_GREEN + 0x10, + CON_COLOR_BRIGHT_YELLOW = CON_COLOR_YELLOW + 0x10, + CON_COLOR_BRIGHT_BLUE = CON_COLOR_BLUE + 0x10, + CON_COLOR_BRIGHT_MAGENTA = CON_COLOR_MAGENTA + 0x10, + CON_COLOR_BRIGHT_CYAN = CON_COLOR_CYAN + 0x10, + CON_COLOR_WHITE = CON_COLOR_LIGHT_GREY + 0x10, + } color:8, background:8; + bool bold; + bool italic; + bool underline; +} CON_attributes_t; + +#ifndef __printflike +/* Missing from Linux headers. */ +#define __printflike(fmtarg, firstvararg) \ +__attribute__((__format__ (__printf__, fmtarg, firstvararg))) +#endif + +void CON_print(const char *string); +void CON_attributed_print(const char *string, CON_attributes_t *attributes); +void CON_vprintf(const char *fmt, va_list args); +void CON_attributed_vprintf(const char *fmt, CON_attributes_t *attributes, va_list args); +void CON_printf(const char *fmt, ...) __printflike(1, 2); +void CON_attributed_printf(const char *fmt, CON_attributes_t *attributes,...) __printflike(1, 3); +void CON_set_async_prompt(const char *string); +void CON_set_repeat_empty(bool repeat); diff --git a/SDL/font.c b/SDL/font.c index 93f3fa9..ea2c590 100644 --- a/SDL/font.c +++ b/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/SDL/font.h b/SDL/font.h index 21753a8..f2111c3 100644 --- a/SDL/font.h +++ b/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/SDL/gui.c b/SDL/gui.c index 62656e8..d5c899b 100644 --- a/SDL/gui.c +++ b/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) @@ -345,7 +388,7 @@ static void cycle_model_backwards(unsigned index) const char *current_model_string(unsigned index) { - return (const char *[]){"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy"} + return (const char *[]){"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy", "Game Boy Pocket"} [configuration.model]; } @@ -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) @@ -991,6 +1160,12 @@ void run_gui(bool is_running) static SDL_Surface *converted_background = NULL; if (!converted_background) { SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp")); + + /* Create a blank background if background.bmp could not be loaded */ + if (!background) { + background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0); + } + SDL_SetPaletteColors(background->format->palette, gui_palette, 0, 4); converted_background = SDL_ConvertSurface(background, pixel_format, 0); SDL_LockSurface(converted_background); @@ -1017,6 +1192,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 +1344,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_save_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 +1392,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 +1430,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 +1456,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 +1471,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 +1523,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 +1548,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 +1579,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/SDL/gui.h b/SDL/gui.h index c950907..1fe8a54 100644 --- a/SDL/gui.h +++ b/SDL/gui.h @@ -39,16 +39,18 @@ 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, JOYPAD_BUTTON_RIGHT, + JOYPAD_BUTTON_LEFT, JOYPAD_BUTTON_UP, JOYPAD_BUTTON_DOWN, JOYPAD_BUTTON_A, @@ -86,6 +88,7 @@ typedef struct { MODEL_CGB, MODEL_AGB, MODEL_SGB, + MODEL_MGB, MODEL_MAX, } model; @@ -110,6 +113,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 +135,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/SDL/main.c b/SDL/main.c index e79d0b3..df53ce0 100644 --- a/SDL/main.c +++ b/SDL/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -9,6 +10,7 @@ #include "gui.h" #include "shader.h" #include "audio/audio.h" +#include "console.h" #ifndef _WIN32 #include @@ -16,6 +18,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 +28,14 @@ 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; +static bool console_supported = false; +bool uses_gl(void) +{ + return gl_context; +} void set_filename(const char *new_filename, typeof(free) *new_free_function) { @@ -37,6 +46,80 @@ void set_filename(const char *new_filename, typeof(free) *new_free_function) free_function = new_free_function; } +static char *completer(const char *substring, uintptr_t *context) +{ + if (!GB_is_inited(&gb)) return NULL; + char *temp = strdup(substring); + char *ret = GB_debugger_complete_substring(&gb, temp, context); + free(temp); + return ret; +} + +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + CON_attributes_t con_attributes = {0,}; + con_attributes.bold = attributes & GB_LOG_BOLD; + con_attributes.underline = attributes & GB_LOG_UNDERLINE; + if (attributes & GB_LOG_DASHED_UNDERLINE) { + while (*string) { + con_attributes.underline ^= true; + CON_attributed_printf("%c", &con_attributes, *string); + string++; + } + } + else { + CON_attributed_print(string, &con_attributes); + } +} + +static void handle_eof(void) +{ + CON_set_async_prompt(""); + char *line = CON_readline("Quit? [y]/n > "); + if (line[0] == 'n' || line[0] == 'N') { + free(line); + CON_set_async_prompt("> "); + } + else { + exit(0); + } +} + +static char *input_callback(GB_gameboy_t *gb) +{ +retry: { + char *ret = CON_readline("Stopped> "); + if (strcmp(ret, CON_EOF) == 0) { + handle_eof(); + free(ret); + goto retry; + } + else { + CON_attributes_t attributes = {.bold = true}; + CON_attributed_printf("> %s\n", &attributes, ret); + } + return ret; +} +} + +static char *asyc_input_callback(GB_gameboy_t *gb) +{ +retry: { + char *ret = CON_readline_async(); + if (ret && strcmp(ret, CON_EOF) == 0) { + handle_eof(); + free(ret); + goto retry; + } + else if (ret) { + CON_attributes_t attributes = {.bold = true}; + CON_attributed_printf("> %s\n", &attributes, ret); + } + return ret; +} +} + + static char *captured_log = NULL; static void log_capture_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) @@ -58,16 +141,16 @@ 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); + GB_set_log_callback(&gb, console_supported? log_callback : NULL); if (captured_log[0] == 0) { free(captured_log); captured_log = NULL; } 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 +203,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 +218,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_save_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 +268,7 @@ static void handle_events(GB_gameboy_t *gb) open_menu(); } } - break; + break; case SDL_JOYAXISMOTION: { static bool axis_active[2] = {false, false}; @@ -215,33 +308,32 @@ 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; } case SDL_SCANCODE_C: if (event.type == SDL_KEYDOWN && (event.key.keysym.mod & KMOD_CTRL)) { + CON_print("^C\a\n"); GB_debugger_break(gb); - } break; @@ -261,7 +353,7 @@ static void handle_events(GB_gameboy_t *gb) } break; } - + case SDL_SCANCODE_P: if (event.key.keysym.mod & MODIFIER) { paused = !paused; @@ -277,14 +369,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 +425,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 +444,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 +478,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); @@ -374,12 +485,15 @@ static void rumble(GB_gameboy_t *gb, double amp) static void debugger_interrupt(int ignore) { - if (!GB_is_inited(&gb)) return; + if (!GB_is_inited(&gb)) exit(0); /* ^C twice to exit */ if (GB_debugger_is_stopped(&gb)) { GB_save_battery(&gb, battery_save_path_ptr); exit(0); } + if (console_supported) { + CON_print("^C\n"); + } GB_debugger_break(&gb); } @@ -388,15 +502,15 @@ static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) if (turbo_down) { static unsigned skip = 0; skip++; - if (skip == GB_audio_get_frequency() / 8) { + if (skip == GB_audio_get_sample_rate() / 8) { skip = 0; } - if (skip > GB_audio_get_frequency() / 16) { + if (skip > GB_audio_get_sample_rate() / 16) { return; } } - if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_frequency() / 4) { + if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_sample_rate() / 4) { return; } @@ -408,28 +522,55 @@ static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) GB_audio_queue_sample(sample); } - 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,20 +589,27 @@ 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_0] = "dmg0_boot.bin", [GB_BOOT_ROM_DMG] = "dmg_boot.bin", [GB_BOOT_ROM_MGB] = "mgb_boot.bin", [GB_BOOT_ROM_SGB] = "sgb_boot.bin", [GB_BOOT_ROM_SGB2] = "sgb2_boot.bin", - [GB_BOOT_ROM_CGB0] = "cgb0_boot.bin", + [GB_BOOT_ROM_CGB_0] = "cgb0_boot.bin", [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) @@ -475,6 +623,7 @@ restart: [MODEL_DMG] = GB_MODEL_DMG_B, [MODEL_CGB] = GB_MODEL_CGB_E, [MODEL_AGB] = GB_MODEL_AGB, + [MODEL_MGB] = GB_MODEL_MGB, [MODEL_SGB] = (GB_model_t []) { [SGB_NTSC] = GB_MODEL_SGB_NTSC, @@ -495,16 +644,30 @@ restart: GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_rumble_callback(&gb, rumble); GB_set_rumble_mode(&gb, configuration.rumble_mode); - GB_set_sample_rate(&gb, GB_audio_get_frequency()); + GB_set_sample_rate(&gb, GB_audio_get_sample_rate()); 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 (console_supported) { + CON_set_async_prompt("> "); + GB_set_log_callback(&gb, log_callback); + GB_set_input_callback(&gb, input_callback); + GB_set_async_input_callback(&gb, asyc_input_callback); + } + } + if (stop_on_start) { + stop_on_start = false; + GB_debugger_break(&gb); } bool error = false; @@ -514,9 +677,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 +693,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 */ @@ -600,19 +769,30 @@ static bool get_arg_flag(const char *flag, int *argc, char **argv) return false; } +#ifdef __APPLE__ +#include +static void enable_smooth_scrolling(void) +{ + CFPreferencesSetAppValue(CFSTR("AppleMomentumScrollSupported"), kCFBooleanTrue, kCFPreferencesCurrentApplication); +} +#endif + 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"); - - bool fullscreen = get_arg_flag("--fullscreen", &argc, argv); +#ifdef __APPLE__ + enable_smooth_scrolling(); +#endif - if (argc > 2) { - fprintf(stderr, "Usage: %s [--fullscreen] [rom]\n", argv[0]); + 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 || (argc == 2 && argv[1][0] == '-')) { + fprintf(stderr, "SameBoy v" GB_VERSION "\n"); + fprintf(stderr, "Usage: %s [--fullscreen|-f] [--nogl] [--stop-debugger|-s] [rom]\n", argv[0]); exit(1); } @@ -623,6 +803,13 @@ int main(int argc, char **argv) signal(SIGINT, debugger_interrupt); SDL_Init(SDL_INIT_EVERYTHING); + if ((console_supported = CON_start(completer))) { + CON_set_repeat_empty(true); + CON_printf("SameBoy v" GB_VERSION "\n"); + } + else { + fprintf(stderr, "SameBoy v" GB_VERSION "\n"); + } strcpy(prefs_path, resource_path("prefs.bin")); if (access(prefs_path, R_OK | W_OK) != 0) { @@ -637,7 +824,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 +834,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 +852,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; } @@ -691,7 +886,7 @@ int main(int argc, char **argv) pixel_format = SDL_AllocFormat(SDL_PIXELFORMAT_ABGR8888); } - GB_audio_init(); + GB_audio_init(0); SDL_EventState(SDL_DROPFILE, SDL_ENABLE); @@ -701,6 +896,7 @@ int main(int argc, char **argv) update_viewport(); if (filename == NULL) { + stop_on_start = false; run_gui(false); } else { diff --git a/SDL/shader.c b/SDL/shader.c index de2ba56..44de290 100644 --- a/SDL/shader.c +++ b/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/SDL/utils.c b/SDL/utils.c index 8cdd00b..603e34a 100644 --- a/SDL/utils.c +++ b/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/SDL/utils.h b/SDL/utils.h index 216e723..5c0383d 100644 --- a/SDL/utils.h +++ b/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/Shaders/MasterShader.metal b/Shaders/MasterShader.metal index b900176..2f3113e 100644 --- a/Shaders/MasterShader.metal +++ b/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/Tester/main.c b/Tester/main.c index 16dbf7b..a3add10 100755 --- a/Tester/main.c +++ b/Tester/main.c @@ -22,28 +22,36 @@ 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 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, }; -uint32_t bitmap[160*144]; +static 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[256*224]; static char *async_input_callback(GB_gameboy_t *gb) { @@ -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; } @@ -129,18 +140,43 @@ static void vblank(GB_gameboy_t *gb) if (frames >= test_length && !gb->disable_rendering) { bool is_screen_blank = true; - for (unsigned i = 160*144; i--;) { - if (bitmap[i] != bitmap[0]) { - is_screen_blank = false; - break; + if (!gb->sgb) { + for (unsigned i = 160 * 144; i--;) { + if (bitmap[i] != bitmap[0]) { + is_screen_blank = false; + break; + } + } + } + else { + if (gb->sgb->mask_mode == 0) { + for (unsigned i = 160 * 144; i--;) { + if (gb->sgb->screen_buffer[i] != gb->sgb->screen_buffer[0]) { + is_screen_blank = false; + break; + } + } } } /* 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); - fwrite(&bitmap, 1, sizeof(bitmap), f); + if (use_tga) { + tga_header[0xC] = GB_get_screen_width(gb); + tga_header[0xD] = GB_get_screen_width(gb) >> 8; + tga_header[0xE] = GB_get_screen_height(gb); + tga_header[0xF] = GB_get_screen_height(gb) >> 8; + fwrite(&tga_header, 1, sizeof(tga_header), f); + } + else { + (*(uint32_t *)&bmp_header[0x2]) = sizeof(bmp_header) + sizeof(bitmap[0]) * GB_get_screen_width(gb) * GB_get_screen_height(gb) + 2; + (*(uint32_t *)&bmp_header[0x12]) = GB_get_screen_width(gb); + (*(int32_t *)&bmp_header[0x16]) = -GB_get_screen_height(gb); + (*(uint32_t *)&bmp_header[0x22]) = sizeof(bitmap[0]) * GB_get_screen_width(gb) * GB_get_screen_height(gb) + 2; + fwrite(&bmp_header, 1, sizeof(bmp_header), f); + } + fwrite(&bitmap, 1, sizeof(bitmap[0]) * GB_get_screen_width(gb) * GB_get_screen_height(gb), f); fclose(f); if (!gb->boot_rom_finished) { GB_log(gb, "Boot ROM did not finish.\n"); @@ -148,6 +184,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 +254,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 +288,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] [--sgb] [--cgb] [--start] [--length seconds] [--sav] [--boot path to boot ROM]" #ifndef _WIN32 " [--jobs number of tests to run simultaneously]" #endif @@ -258,6 +305,8 @@ int main(int argc, char **argv) #endif bool dmg = false; + bool sgb = false; + bool sav = false; const char *boot_rom_path = NULL; GB_random_set_enabled(false); @@ -266,6 +315,27 @@ int main(int argc, char **argv) if (strcmp(argv[i], "--dmg") == 0) { fprintf(stderr, "Using DMG mode\n"); dmg = true; + sgb = false; + continue; + } + + if (strcmp(argv[i], "--sgb") == 0) { + fprintf(stderr, "Using SGB mode\n"); + sgb = true; + dmg = false; + continue; + } + + if (strcmp(argv[i], "--cgb") == 0) { + fprintf(stderr, "Using CGB mode\n"); + dmg = false; + sgb = false; + continue; + } + + if (strcmp(argv[i], "--tga") == 0) { + fprintf(stderr, "Using TGA output\n"); + use_tga = true; continue; } @@ -287,6 +357,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 +388,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) { @@ -328,6 +410,13 @@ int main(int argc, char **argv) exit(1); } } + else if (sgb) { + GB_init(&gb, GB_MODEL_SGB2); + if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("sgb2_boot.bin"))) { + fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("sgb2_boot.bin")); + exit(1); + } + } else { GB_init(&gb, GB_MODEL_CGB_E); if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("cgb_boot.bin"))) { @@ -342,6 +431,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 +450,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 +490,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/Windows/pthread.h b/Windows/pthread.h new file mode 100644 index 0000000..7bf328b --- /dev/null +++ b/Windows/pthread.h @@ -0,0 +1,86 @@ +/* Very minimal pthread implementation for Windows */ +#include +#include + +typedef HANDLE pthread_t; +typedef struct pthread_attr_s pthread_attr_t; + +static inline int pthread_create(pthread_t *pthread, + const pthread_attr_t *attrs, + LPTHREAD_START_ROUTINE function, + void *context) +{ + assert(!attrs); + *pthread = CreateThread(NULL, 0, function, + context, 0, NULL); + return *pthread? 0 : GetLastError(); +} + + +typedef struct { + unsigned status; + CRITICAL_SECTION cs; +} pthread_mutex_t; +#define PTHREAD_MUTEX_INITIALIZER {0,} + +static inline CRITICAL_SECTION *pthread_mutex_to_cs(pthread_mutex_t *mutex) +{ +retry: + if (mutex->status == 2) return &mutex->cs; + + unsigned expected = 0; + unsigned desired = 1; + if (__atomic_compare_exchange(&mutex->status, &expected, &desired, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + InitializeCriticalSection(&mutex->cs); + mutex->status = 2; + return &mutex->cs; + } + goto retry; +} + +static inline int pthread_mutex_lock(pthread_mutex_t *mutex) +{ + EnterCriticalSection(pthread_mutex_to_cs(mutex)); + return 0; +} + +static inline int pthread_mutex_unlock(pthread_mutex_t *mutex) +{ + LeaveCriticalSection(pthread_mutex_to_cs(mutex)); + return 0; +} + +typedef struct { + unsigned status; + CONDITION_VARIABLE cond; +} pthread_cond_t; +#define PTHREAD_COND_INITIALIZER {0,} + +static inline CONDITION_VARIABLE *pthread_cond_to_win(pthread_cond_t *cond) +{ +retry: + if (cond->status == 2) return &cond->cond; + + unsigned expected = 0; + unsigned desired = 1; + if (__atomic_compare_exchange(&cond->status, &expected, &desired, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + InitializeConditionVariable(&cond->cond); + cond->status = 2; + return &cond->cond; + } + goto retry; +} + + +static inline int pthread_cond_signal(pthread_cond_t *cond) +{ + WakeConditionVariable(pthread_cond_to_win(cond)); + return 0; +} + +static inline int pthread_cond_wait(pthread_cond_t *cond, + pthread_mutex_t *mutex) +{ + // Mutex is locked therefore already initialized + return SleepConditionVariableCS(pthread_cond_to_win(cond), &mutex->cs, INFINITE); +} diff --git a/Windows/sameboy.ico b/Windows/sameboy.ico index 685523e..bd8e372 100644 Binary files a/Windows/sameboy.ico and b/Windows/sameboy.ico differ diff --git a/Windows/stdio.h b/Windows/stdio.h index ef21ea4..1e6ec02 100755 --- a/Windows/stdio.h +++ b/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/Windows/unistd.h b/Windows/unistd.h index b7aabf2..62e2337 100644 --- a/Windows/unistd.h +++ b/Windows/unistd.h @@ -5,3 +5,4 @@ #define read(...) _read(__VA_ARGS__) #define write(...) _write(__VA_ARGS__) +#define isatty(...) _isatty(__VA_ARGS__) diff --git a/build-faq.md b/build-faq.md index 2b056dd..0921436 100644 --- a/build-faq.md +++ b/build-faq.md @@ -1,7 +1,57 @@ -# Attempting to build the Cocoa frontend fails with NSInternalInconsistencyException +# macOS Specific Issues +## Attempting to build the Cocoa frontend fails with NSInternalInconsistencyException When building on macOS, the build system will make a native Cocoa app by default. In this case, the build system uses the Xcode `ibtool` command to build user interface files. If this command fails, you can fix this issue by starting Xcode and letting it install components. After this is done, you should be able to close Xcode and build successfully. -# Attempting to build the SDL frontend on macOS fails on linking +## Attempting to build the SDL frontend on macOS fails on linking -SameBoy on macOS expects you to have SDL2 installed via Brew, and not as a framework. Older versions expected it to be installed as a framework, but this is no longer the case. \ No newline at end of file +SameBoy on macOS expects you to have SDL2 installed via Brew, and not as a framework. Older versions expected it to be installed as a framework, but this is no longer the case. + +# Windows Build Process + +## Tools and Libraries Installation + +For the various tools and libraries, follow the below guide to ensure easy, proper configuration for the build environment: + +### SDL2 + +For [libSDL2](https://libsdl.org/download-2.0.php), download the Visual C++ Development Library pack. Place the extracted files within a known folder for later. Both the `\x86\` and `\include\` paths will be needed. + +The following examples will be referenced later: + +- `C:\SDL2\lib\x86\*` +- `C:\SDL2\include\*` + +### 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 + +Ensure that the `gnuwin32\bin\` directory is included in `%PATH%`. Like rgbds above, this may instead be manually included on the command line before installation: `set path=%path%;C:\path\to\gnuwin32\bin`. + +## Building + +Within a command prompt in the project directory: + +``` +vcvars32 +set lib=%lib%;C:\SDL2\lib\x86 +set include=%include%;C:\SDL2\include +make +``` +Please note that these directories (`C:\SDL2\*`) are the examples given within the "SDL Port" section above. Ensure that your `%PATH%` properly includes `rgbds` and `gnuwin32\bin`, and that the `lib` and `include` paths include the appropriate SDL2 directories. + +## Common Errors + +### Error -1073741819 + +If encountering an error that appears as follows: + +``` make: *** [build/bin/BootROMs/dmg_boot.bin] Error -1073741819``` + +Simply run `make` again, and the process will continue. This appears to happen occasionally with `build/bin/BootROMs/dmg_boot.bin` and `build/bin/BootROMs/sgb2_boot.bin`. It does not affect the compiled output. This appears to be an issue with GnuWin. + +### The system cannot find the file specified (`usr/bin/mkdir`) + +If errors arise (i.e., particularly with the `CREATE_PROCESS('usr/bin/mkdir')` calls, also verify that Git for Windows has not been installed with full Linux support. If it has, remove `C:\Program Files\Git\usr\bin` from the SYSTEM %PATH% until after compilation. This happens because the Git for Windows version of `which` is used instead of the GnuWin one, and it returns a Unix-style path instead of a Windows one. diff --git a/libretro/Makefile b/libretro/Makefile index b327628..ada200d 100644 --- a/libretro/Makefile +++ b/libretro/Makefile @@ -17,8 +17,6 @@ filter_out2 = $(call filter_out1,$(call filter_out1,$1)) unixpath = $(subst \,/,$1) unixcygpath = /$(subst :,,$(call unixpath,$1)) -CFLAGS += -DSAMEBOY_CORE_VERSION=\"$(VERSION)\" - ifeq ($(platform),) platform = unix ifeq ($(shell uname -a),) @@ -51,7 +49,7 @@ ifeq ($(platform), win) INCFLAGS += -I Windows endif -CORE_DIR += .. +CORE_DIR = ../ TARGET_NAME = sameboy LIBM = -lm @@ -90,18 +88,69 @@ else ifeq ($(platform), linux-portable) TARGET := $(TARGET_NAME)_libretro.$(EXT) fpic := -fPIC -nostdlib SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T - LIBM := + LIBM := +# (armv7 a7, hard point, neon based) ### +# NESC, SNESC, C64 mini +else ifeq ($(platform), classic_armv7_a7) + TARGET := $(TARGET_NAME)_libretro.so + fpic := -fPIC + SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined + CFLAGS += -Ofast \ + -flto=4 -fwhole-program -fuse-linker-plugin \ + -fdata-sections -ffunction-sections -Wl,--gc-sections \ + -fno-stack-protector -fno-ident -fomit-frame-pointer \ + -falign-functions=1 -falign-jumps=1 -falign-loops=1 \ + -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops \ + -fmerge-all-constants -fno-math-errno \ + -marm -mtune=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard + CXXFLAGS += $(CFLAGS) + CPPFLAGS += $(CFLAGS) + ASFLAGS += $(CFLAGS) + HAVE_NEON = 1 + ARCH = arm + BUILTIN_GPU = neon + USE_DYNAREC = 1 + ifeq ($(shell echo `$(CC) -dumpversion` "< 4.9" | bc -l), 1) + CFLAGS += -march=armv7-a + else + CFLAGS += -march=armv7ve + # If gcc is 5.0 or later + ifeq ($(shell echo `$(CC) -dumpversion` ">= 5" | bc -l), 1) + 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) TARGET := $(TARGET_NAME)_libretro_$(platform).a include $(LIBTRANSISTOR_HOME)/libtransistor.mk CFLAGS += -Wl,-q -O3 -fno-short-enums -fno-optimize-sibling-calls STATIC_LINKING=1 +# Nintendo Switch (libnx) +else ifeq ($(platform), libnx) + include $(DEVKITPRO)/libnx/switch_rules + TARGET := $(TARGET_NAME)_libretro_$(platform).a + DEFINES += -DSWITCH=1 -D__SWITCH__ -DARM + CFLAGS += $(DEFINES) -fPIE -I$(LIBNX)/include/ -ffunction-sections -fdata-sections -ftls-model=local-exec + CFLAGS += -march=armv8-a -mtune=cortex-a57 -mtp=soft -mcpu=cortex-a57+crc+fp+simd -ffast-math + CXXFLAGS := $(ASFLAGS) $(CFLAGS) + STATIC_LINKING = 1 # Nintendo WiiU else ifeq ($(platform), wiiu) TARGET := $(TARGET_NAME)_libretro_$(platform).a - CC = $(DEVKITPPC)/bin/powerpc-eabi-gcc$(EXE_EXT) - AR = $(DEVKITPPC)/bin/powerpc-eabi-ar$(EXE_EXT) + CC ?= $(DEVKITPPC)/bin/powerpc-eabi-gcc$(EXE_EXT) + AR ?= $(DEVKITPPC)/bin/powerpc-eabi-ar$(EXE_EXT) CFLAGS += -DGEKKO -DHW_RVL -DWIIU -mwup -mcpu=750 -meabi -mhard-float -D__ppc__ -DMSB_FIRST -I$(DEVKITPRO)/libogc/include CFLAGS += -U__INT32_TYPE__ -U __UINT32_TYPE__ -D__INT32_TYPE__=int STATIC_LINKING = 1 @@ -140,7 +189,7 @@ else ifeq ($(platform), emscripten) fpic := -fPIC SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined else ifeq ($(platform), vita) - TARGET := $(TARGET_NAME)_vita.a + TARGET := $(TARGET_NAME)_libretro_vita.a CC = arm-vita-eabi-gcc AR = arm-vita-eabi-ar CFLAGS += -Wl,-q -O3 -fno-short-enums -fno-optimize-sibling-calls @@ -172,14 +221,14 @@ else ifneq (,$(findstring windows_msvc2017,$(platform))) TargetArchMoniker = $(subst $(WinPartition)_,,$(PlatformSuffix)) - CC = cl.exe - CXX = cl.exe - LD = link.exe + CC ?= cl.exe + CXX ?= cl.exe + LD ?= link.exe reg_query = $(call filter_out2,$(subst $2,,$(shell reg query "$2" -v "$1" 2>nul))) fix_path = $(subst $(SPACE),\ ,$(subst \,/,$1)) - ProgramFiles86w := $(shell cmd /c "echo %PROGRAMFILES(x86)%") + ProgramFiles86w := $(shell cmd //c "echo %PROGRAMFILES(x86)%") ProgramFiles86 := $(shell cygpath "$(ProgramFiles86w)") WindowsSdkDir ?= $(call reg_query,InstallationFolder,HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0) @@ -242,7 +291,7 @@ else ifneq (,$(findstring windows_msvc2017,$(platform))) LDFLAGS += -DLL else - CC = gcc + CC ?= gcc TARGET := $(TARGET_NAME)_libretro.dll SHARED := -shared -static-libgcc -static-libstdc++ -s -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined endif @@ -262,6 +311,8 @@ endif include Makefile.common +CFLAGS += -DGB_VERSION=\"$(VERSION)\" + OBJECTS := $(patsubst $(CORE_DIR)/%.c,$(CORE_DIR)/build/obj/%_libretro.c.o,$(SOURCES_C)) OBJOUT = -o diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 7f7688a..fabe3ad 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -1,3 +1,5 @@ +include $(CORE_DIR)/version.mk + INCFLAGS := -I$(CORE_DIR) SOURCES_C := $(CORE_DIR)/Core/gb.c \ diff --git a/libretro/jni/Android.mk b/libretro/jni/Android.mk index 3b2d74b..8ac1b3b 100644 --- a/libretro/jni/Android.mk +++ b/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 +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/libretro/libretro.c b/libretro/libretro.c index 24514d4..94b82a6 100644 --- a/libretro/libretro.c +++ b/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; @@ -85,6 +90,8 @@ static retro_audio_sample_t audio_sample_cb; static retro_input_poll_t input_poll_cb; static retro_input_state_t input_state_cb; +static bool libretro_supports_bitmasks = false; + static unsigned emulated_devices = 1; static bool initialized = false; static unsigned screen_layout = 0; @@ -119,31 +126,46 @@ static struct retro_rumble_interface rumble; static void GB_update_keys_status(GB_gameboy_t *gb, unsigned port) { + uint16_t joypad_bits = 0; + input_poll_cb(); + if (libretro_supports_bitmasks) { + joypad_bits = input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK); + } + else { + unsigned 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); + } + } + } + GB_set_key_state_for_player(gb, GB_KEY_RIGHT, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_RIGHT)); GB_set_key_state_for_player(gb, GB_KEY_LEFT, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_LEFT)); GB_set_key_state_for_player(gb, GB_KEY_UP, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_UP)); GB_set_key_state_for_player(gb, GB_KEY_DOWN, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_DOWN)); GB_set_key_state_for_player(gb, GB_KEY_A, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_A)); GB_set_key_state_for_player(gb, GB_KEY_B, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_B)); GB_set_key_state_for_player(gb, GB_KEY_SELECT, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_SELECT)); GB_set_key_state_for_player(gb, GB_KEY_START, emulated_devices == 1 ? port : 0, - input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START)); + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_START)); } 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); } @@ -196,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; @@ -203,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; 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; Auto|Game Boy|Game Boy Color|Game Boy Advance" }, - { "sameboy_model_2", "Emulated model for Game Boy #2; 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 }, @@ -329,56 +397,59 @@ 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 *[]){ - [GB_BOOT_ROM_DMG0] = "dmg0", + const char *model_name = (char *[]) { + [GB_BOOT_ROM_DMG_0] = "dmg0", [GB_BOOT_ROM_DMG] = "dmg", [GB_BOOT_ROM_MGB] = "mgb", [GB_BOOT_ROM_SGB] = "sgb", [GB_BOOT_ROM_SGB2] = "sgb2", - [GB_BOOT_ROM_CGB0] = "cgb0", + [GB_BOOT_ROM_CGB_0] = "cgb0", [GB_BOOT_ROM_CGB] = "cgb", [GB_BOOT_ROM_AGB] = "agb", }[type]; - - const uint8_t *boot_code = (const unsigned char *[]) - { - [GB_BOOT_ROM_DMG0] = dmg_boot, // dmg0 not implemented yet + + const uint8_t *boot_code = (const unsigned char *[]) { + [GB_BOOT_ROM_DMG_0] = dmg_boot, // DMG_0 not implemented yet [GB_BOOT_ROM_DMG] = dmg_boot, [GB_BOOT_ROM_MGB] = dmg_boot, // mgb not implemented yet [GB_BOOT_ROM_SGB] = sgb_boot, [GB_BOOT_ROM_SGB2] = sgb2_boot, - [GB_BOOT_ROM_CGB0] = cgb_boot, // cgb0 not implemented yet + [GB_BOOT_ROM_CGB_0] = cgb_boot, // CGB_0 not implemented yet [GB_BOOT_ROM_CGB] = cgb_boot, [GB_BOOT_ROM_AGB] = agb_boot, }[type]; - - unsigned boot_length = (unsigned []){ - [GB_BOOT_ROM_DMG0] = dmg_boot_length, // dmg0 not implemented yet + + unsigned boot_length = (unsigned []) { + [GB_BOOT_ROM_DMG_0] = dmg_boot_length, // DMG_0 not implemented yet [GB_BOOT_ROM_DMG] = dmg_boot_length, [GB_BOOT_ROM_MGB] = dmg_boot_length, // mgb not implemented yet [GB_BOOT_ROM_SGB] = sgb_boot_length, [GB_BOOT_ROM_SGB2] = sgb2_boot_length, - [GB_BOOT_ROM_CGB0] = cgb_boot_length, // cgb0 not implemented yet + [GB_BOOT_ROM_CGB_0] = cgb_boot_length, // CGB_0 not implemented yet [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); @@ -495,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 }, @@ -506,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 }, @@ -514,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 }, @@ -523,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); } @@ -548,64 +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; - } - - if (new_model != model[0]) { - geometry_updated = true; - model[0] = new_model; - init_for_current_model(0); - } + GB_set_light_temperature(&gameboy[0], atof(var.value)); } var.key = "sameboy_border"; @@ -620,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) { @@ -681,123 +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; - } - - if (model[0] != new_model) { - model[0] = new_model; - init_for_current_model(0); - } - } - - 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; - } - - if (model[1] != new_model) { - model[1] = new_model; - init_for_current_model(1); - } - } - - 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; @@ -813,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; } @@ -823,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) @@ -850,6 +1040,10 @@ void retro_init(void) else { log_cb = fallback_log; } + + if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL)) { + libretro_supports_bitmasks = true; + } } void retro_deinit(void) @@ -858,6 +1052,8 @@ void retro_deinit(void) free(frame_buf_copy); frame_buf = NULL; frame_buf_copy = NULL; + + libretro_supports_bitmasks = false; } unsigned retro_api_version(void) @@ -875,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"; @@ -888,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; @@ -900,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]); @@ -918,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); } @@ -947,10 +1145,14 @@ void retro_set_video_refresh(retro_video_refresh_t cb) void retro_reset(void) { + check_variables(); + for (int i = 0; i < emulated_devices; i++) { + init_for_current_model(i); GB_reset(&gameboy[i]); } + geometry_updated = true; } void retro_run(void) @@ -977,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); } @@ -992,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]); @@ -1002,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]), @@ -1040,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; } @@ -1083,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]); } } @@ -1102,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)); @@ -1112,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; } @@ -1148,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; } @@ -1180,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; @@ -1191,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; @@ -1200,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; } @@ -1212,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; @@ -1240,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; @@ -1285,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; @@ -1313,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/libretro/libretro.h b/libretro/libretro.h index a4df6be..e6ee626 100644 --- a/libretro/libretro.h +++ b/libretro/libretro.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2010-2016 The RetroArch team +/* Copyright (C) 2010-2020 The RetroArch team * * --------------------------------------------------------------------------------------- * The following license statement only applies to this libretro API header (libretro.h). @@ -32,7 +32,7 @@ extern "C" { #endif #ifndef __cplusplus -#if defined(_MSC_VER) && !defined(SN_TARGET_PS3) +#if defined(_MSC_VER) && _MSC_VER < 1800 && !defined(SN_TARGET_PS3) /* Hack applied for MSVC when compiling in C89 mode * as it isn't C99-compliant. */ #define bool unsigned char @@ -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 @@ -77,7 +77,7 @@ extern "C" { # endif #endif -/* Used for checking API/ABI mismatches that can break libretro +/* Used for checking API/ABI mismatches that can break libretro * implementations. * It is not incremented for compatible changes to the API. */ @@ -87,13 +87,13 @@ extern "C" { * Libretro's fundamental device abstractions. * * Libretro's input system consists of some standardized device types, - * such as a joypad (with/without analog), mouse, keyboard, lightgun + * such as a joypad (with/without analog), mouse, keyboard, lightgun * and a pointer. * - * The functionality of these devices are fixed, and individual cores + * The functionality of these devices are fixed, and individual cores * map their own concept of a controller to libretro's abstractions. - * This makes it possible for frontends to map the abstract types to a - * real input device, and not having to worry about binding input + * This makes it possible for frontends to map the abstract types to a + * real input device, and not having to worry about binding input * correctly to arbitrary controller layouts. */ @@ -104,43 +104,52 @@ extern "C" { /* Input disabled. */ #define RETRO_DEVICE_NONE 0 -/* The JOYPAD is called RetroPad. It is essentially a Super Nintendo - * controller, but with additional L2/R2/L3/R3 buttons, similar to a +/* The JOYPAD is called RetroPad. It is essentially a Super Nintendo + * controller, but with additional L2/R2/L3/R3 buttons, similar to a * PS1 DualShock. */ #define RETRO_DEVICE_JOYPAD 1 /* The mouse is a simple mouse, similar to Super Nintendo's mouse. * X and Y coordinates are reported relatively to last poll (poll callback). - * It is up to the libretro implementation to keep track of where the mouse + * It is up to the libretro implementation to keep track of where the mouse * pointer is supposed to be on the screen. - * The frontend must make sure not to interfere with its own hardware + * The frontend must make sure not to interfere with its own hardware * mouse pointer. */ #define RETRO_DEVICE_MOUSE 2 /* KEYBOARD device lets one poll for raw key pressed. - * It is poll based, so input callback will return with the current + * It is poll based, so input callback will return with the current * pressed state. * For event/text based keyboard input, see * RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK. */ #define RETRO_DEVICE_KEYBOARD 3 -/* Lightgun X/Y coordinates are reported relatively to last poll, - * similar to mouse. */ +/* LIGHTGUN device is similar to Guncon-2 for PlayStation 2. + * It reports X/Y coordinates in screen space (similar to the pointer) + * in the range [-0x8000, 0x7fff] in both axes, with zero being center and + * -0x8000 being out of bounds. + * As well as reporting on/off screen state. It features a trigger, + * start/select buttons, auxiliary action buttons and a + * directional pad. A forced off-screen shot can be requested for + * auto-reloading function in some games. + */ #define RETRO_DEVICE_LIGHTGUN 4 /* The ANALOG device is an extension to JOYPAD (RetroPad). - * Similar to DualShock it adds two analog sticks. - * This is treated as a separate device type as it returns values in the - * full analog range of [-0x8000, 0x7fff]. Positive X axis is right. - * Positive Y axis is down. - * Only use ANALOG type when polling for analog values of the axes. + * Similar to DualShock2 it adds two analog sticks and all buttons can + * be analog. This is treated as a separate device type as it returns + * axis values in the full analog range of [-0x7fff, 0x7fff], + * although some devices may return -0x8000. + * Positive X axis is right. Positive Y axis is down. + * Buttons are returned in the range [0, 0x7fff]. + * Only use ANALOG type when polling for analog values. */ #define RETRO_DEVICE_ANALOG 5 /* Abstracts the concept of a pointing mechanism, e.g. touch. - * This allows libretro to query in absolute coordinates where on the + * This allows libretro to query in absolute coordinates where on the * screen a mouse (or something similar) is being placed. * For a touch centric device, coordinates reported are the coordinates * of the press. @@ -148,33 +157,34 @@ extern "C" { * Coordinates in X and Y are reported as: * [-0x7fff, 0x7fff]: -0x7fff corresponds to the far left/top of the screen, * and 0x7fff corresponds to the far right/bottom of the screen. - * The "screen" is here defined as area that is passed to the frontend and + * The "screen" is here defined as area that is passed to the frontend and * later displayed on the monitor. * * The frontend is free to scale/resize this screen as it sees fit, however, - * (X, Y) = (-0x7fff, -0x7fff) will correspond to the top-left pixel of the + * (X, Y) = (-0x7fff, -0x7fff) will correspond to the top-left pixel of the * game image, etc. * - * To check if the pointer coordinates are valid (e.g. a touch display + * To check if the pointer coordinates are valid (e.g. a touch display * actually being touched), PRESSED returns 1 or 0. * - * If using a mouse on a desktop, PRESSED will usually correspond to the + * If using a mouse on a desktop, PRESSED will usually correspond to the * left mouse button, but this is a frontend decision. * PRESSED will only return 1 if the pointer is inside the game screen. * - * For multi-touch, the index variable can be used to successively query + * For multi-touch, the index variable can be used to successively query * more presses. * If index = 0 returns true for _PRESSED, coordinates can be extracted - * with _X, _Y for index = 0. One can then query _PRESSED, _X, _Y with + * with _X, _Y for index = 0. One can then query _PRESSED, _X, _Y with * index = 1, and so on. - * Eventually _PRESSED will return false for an index. No further presses + * Eventually _PRESSED will return false for an index. No further presses * are registered at this point. */ #define RETRO_DEVICE_POINTER 6 /* Buttons for the RetroPad (JOYPAD). - * The placement of these is equivalent to placements on the + * The placement of these is equivalent to placements on the * Super Nintendo controller. - * L2/R2/L3/R3 buttons correspond to the PS1 DualShock. */ + * L2/R2/L3/R3 buttons correspond to the PS1 DualShock. + * Also used as id values for RETRO_DEVICE_INDEX_ANALOG_BUTTON */ #define RETRO_DEVICE_ID_JOYPAD_B 0 #define RETRO_DEVICE_ID_JOYPAD_Y 1 #define RETRO_DEVICE_ID_JOYPAD_SELECT 2 @@ -192,11 +202,14 @@ extern "C" { #define RETRO_DEVICE_ID_JOYPAD_L3 14 #define RETRO_DEVICE_ID_JOYPAD_R3 15 +#define RETRO_DEVICE_ID_JOYPAD_MASK 256 + /* Index / Id values for ANALOG device. */ -#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0 -#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1 -#define RETRO_DEVICE_ID_ANALOG_X 0 -#define RETRO_DEVICE_ID_ANALOG_Y 1 +#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0 +#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1 +#define RETRO_DEVICE_INDEX_ANALOG_BUTTON 2 +#define RETRO_DEVICE_ID_ANALOG_X 0 +#define RETRO_DEVICE_ID_ANALOG_Y 1 /* Id values for MOUSE. */ #define RETRO_DEVICE_ID_MOUSE_X 0 @@ -208,20 +221,36 @@ extern "C" { #define RETRO_DEVICE_ID_MOUSE_MIDDLE 6 #define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7 #define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8 +#define RETRO_DEVICE_ID_MOUSE_BUTTON_4 9 +#define RETRO_DEVICE_ID_MOUSE_BUTTON_5 10 -/* Id values for LIGHTGUN types. */ -#define RETRO_DEVICE_ID_LIGHTGUN_X 0 -#define RETRO_DEVICE_ID_LIGHTGUN_Y 1 -#define RETRO_DEVICE_ID_LIGHTGUN_TRIGGER 2 -#define RETRO_DEVICE_ID_LIGHTGUN_CURSOR 3 -#define RETRO_DEVICE_ID_LIGHTGUN_TURBO 4 -#define RETRO_DEVICE_ID_LIGHTGUN_PAUSE 5 -#define RETRO_DEVICE_ID_LIGHTGUN_START 6 +/* Id values for LIGHTGUN. */ +#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X 13 /*Absolute Position*/ +#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y 14 /*Absolute*/ +#define RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN 15 /*Status Check*/ +#define RETRO_DEVICE_ID_LIGHTGUN_TRIGGER 2 +#define RETRO_DEVICE_ID_LIGHTGUN_RELOAD 16 /*Forced off-screen shot*/ +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_A 3 +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_B 4 +#define RETRO_DEVICE_ID_LIGHTGUN_START 6 +#define RETRO_DEVICE_ID_LIGHTGUN_SELECT 7 +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_C 8 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_UP 9 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_DOWN 10 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_LEFT 11 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_RIGHT 12 +/* deprecated */ +#define RETRO_DEVICE_ID_LIGHTGUN_X 0 /*Relative Position*/ +#define RETRO_DEVICE_ID_LIGHTGUN_Y 1 /*Relative*/ +#define RETRO_DEVICE_ID_LIGHTGUN_CURSOR 3 /*Use Aux:A*/ +#define RETRO_DEVICE_ID_LIGHTGUN_TURBO 4 /*Use Aux:B*/ +#define RETRO_DEVICE_ID_LIGHTGUN_PAUSE 5 /*Use Start*/ /* Id values for POINTER. */ #define RETRO_DEVICE_ID_POINTER_X 0 #define RETRO_DEVICE_ID_POINTER_Y 1 #define RETRO_DEVICE_ID_POINTER_PRESSED 2 +#define RETRO_DEVICE_ID_POINTER_COUNT 3 /* Returned from retro_get_region(). */ #define RETRO_REGION_NTSC 0 @@ -230,28 +259,38 @@ extern "C" { /* Id values for LANGUAGE */ enum retro_language { - RETRO_LANGUAGE_ENGLISH = 0, - RETRO_LANGUAGE_JAPANESE = 1, - RETRO_LANGUAGE_FRENCH = 2, - RETRO_LANGUAGE_SPANISH = 3, - RETRO_LANGUAGE_GERMAN = 4, - RETRO_LANGUAGE_ITALIAN = 5, - RETRO_LANGUAGE_DUTCH = 6, - RETRO_LANGUAGE_PORTUGUESE = 7, - RETRO_LANGUAGE_RUSSIAN = 8, - RETRO_LANGUAGE_KOREAN = 9, - RETRO_LANGUAGE_CHINESE_TRADITIONAL = 10, - RETRO_LANGUAGE_CHINESE_SIMPLIFIED = 11, - RETRO_LANGUAGE_ESPERANTO = 12, - RETRO_LANGUAGE_POLISH = 13, + RETRO_LANGUAGE_ENGLISH = 0, + RETRO_LANGUAGE_JAPANESE = 1, + RETRO_LANGUAGE_FRENCH = 2, + RETRO_LANGUAGE_SPANISH = 3, + RETRO_LANGUAGE_GERMAN = 4, + RETRO_LANGUAGE_ITALIAN = 5, + RETRO_LANGUAGE_DUTCH = 6, + RETRO_LANGUAGE_PORTUGUESE_BRAZIL = 7, + RETRO_LANGUAGE_PORTUGUESE_PORTUGAL = 8, + RETRO_LANGUAGE_RUSSIAN = 9, + RETRO_LANGUAGE_KOREAN = 10, + RETRO_LANGUAGE_CHINESE_TRADITIONAL = 11, + RETRO_LANGUAGE_CHINESE_SIMPLIFIED = 12, + RETRO_LANGUAGE_ESPERANTO = 13, + RETRO_LANGUAGE_POLISH = 14, + RETRO_LANGUAGE_VIETNAMESE = 15, + 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) */ - RETRO_LANGUAGE_DUMMY = INT_MAX + RETRO_LANGUAGE_DUMMY = INT_MAX }; /* Passed to retro_get_memory_data/size(). - * If the memory type doesn't apply to the + * If the memory type doesn't apply to the * implementation NULL/0 can be returned. */ #define RETRO_MEMORY_MASK 0xff @@ -349,6 +388,10 @@ enum retro_key RETROK_x = 120, RETROK_y = 121, RETROK_z = 122, + RETROK_LEFTBRACE = 123, + RETROK_BAR = 124, + RETROK_RIGHTBRACE = 125, + RETROK_TILDE = 126, RETROK_DELETE = 127, RETROK_KP0 = 256, @@ -419,6 +462,7 @@ enum retro_key RETROK_POWER = 320, RETROK_EURO = 321, RETROK_UNDO = 322, + RETROK_OEM_102 = 323, RETROK_LAST, @@ -441,7 +485,7 @@ enum retro_mod RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */ }; -/* If set, this call is not part of the public libretro API yet. It can +/* If set, this call is not part of the public libretro API yet. It can * change or be removed at any time. */ #define RETRO_ENVIRONMENT_EXPERIMENTAL 0x10000 /* Environment callback to be used internally in frontend. */ @@ -450,12 +494,14 @@ enum retro_mod /* Environment commands. */ #define RETRO_ENVIRONMENT_SET_ROTATION 1 /* const unsigned * -- * Sets screen rotation of graphics. - * Is only implemented if rotation can be accelerated by hardware. - * Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180, + * Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180, * 270 degrees counter-clockwise respectively. */ #define RETRO_ENVIRONMENT_GET_OVERSCAN 2 /* bool * -- - * Boolean value whether or not the implementation should use overscan, + * NOTE: As of 2019 this callback is considered deprecated in favor of + * using core options to manage overscan in a more nuanced, core-specific way. + * + * Boolean value whether or not the implementation should use overscan, * or crop away overscan. */ #define RETRO_ENVIRONMENT_GET_CAN_DUPE 3 /* bool * -- @@ -463,15 +509,15 @@ enum retro_mod * passing NULL to video frame callback. */ - /* Environ 4, 5 are no longer supported (GET_VARIABLE / SET_VARIABLES), + /* Environ 4, 5 are no longer supported (GET_VARIABLE / SET_VARIABLES), * and reserved to avoid possible ABI clash. */ #define RETRO_ENVIRONMENT_SET_MESSAGE 6 /* const struct retro_message * -- - * Sets a message to be displayed in implementation-specific manner + * Sets a message to be displayed in implementation-specific manner * for a certain amount of 'frames'. - * Should not be used for trivial messages, which should simply be - * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a + * 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_SHUTDOWN 7 /* N/A (NULL) -- @@ -499,15 +545,15 @@ enum retro_mod #define RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY 9 /* const char ** -- * Returns the "system" directory of the frontend. - * This directory can be used to store system specific + * This directory can be used to store system specific * content such as BIOSes, configuration data, etc. * The returned value can be NULL. * If so, no such directory is defined, * and it's up to the implementation to find a suitable directory. * - * NOTE: Some cores used this folder also for "save" data such as + * NOTE: Some cores used this folder also for "save" data such as * memory cards, etc, for lack of a better place to put it. - * This is now discouraged, and if possible, cores should try to + * This is now discouraged, and if possible, cores should try to * use the new GET_SAVE_DIRECTORY. */ #define RETRO_ENVIRONMENT_SET_PIXEL_FORMAT 10 @@ -515,19 +561,19 @@ enum retro_mod * Sets the internal pixel format used by the implementation. * The default pixel format is RETRO_PIXEL_FORMAT_0RGB1555. * This pixel format however, is deprecated (see enum retro_pixel_format). - * If the call returns false, the frontend does not support this pixel + * If the call returns false, the frontend does not support this pixel * format. * - * This function should be called inside retro_load_game() or + * This function should be called inside retro_load_game() or * retro_get_system_av_info(). */ #define RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS 11 /* const struct retro_input_descriptor * -- * Sets an array of retro_input_descriptors. * It is up to the frontend to present this in a usable way. - * The array is terminated by retro_input_descriptor::description + * The array is terminated by retro_input_descriptor::description * being set to NULL. - * This function can be called at any time, but it is recommended + * This function can be called at any time, but it is recommended * to call it as early as possible. */ #define RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK 12 @@ -536,52 +582,55 @@ enum retro_mod */ #define RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE 13 /* const struct retro_disk_control_callback * -- - * Sets an interface which frontend can use to eject and insert + * Sets an interface which frontend can use to eject and insert * disk images. - * This is used for games which consist of multiple images and + * This is used for games which consist of multiple images and * must be manually swapped out by the user (e.g. PSX). */ #define RETRO_ENVIRONMENT_SET_HW_RENDER 14 /* struct retro_hw_render_callback * -- - * Sets an interface to let a libretro core render with + * Sets an interface to let a libretro core render with * hardware acceleration. * Should be called in retro_load_game(). - * If successful, libretro cores will be able to render to a + * If successful, libretro cores will be able to render to a * frontend-provided framebuffer. - * The size of this framebuffer will be at least as large as + * The size of this framebuffer will be at least as large as * max_width/max_height provided in get_av_info(). - * If HW rendering is used, pass only RETRO_HW_FRAME_BUFFER_VALID or + * If HW rendering is used, pass only RETRO_HW_FRAME_BUFFER_VALID or * NULL to retro_video_refresh_t. */ #define RETRO_ENVIRONMENT_GET_VARIABLE 15 /* struct retro_variable * -- * Interface to acquire user-defined information from environment * that cannot feasibly be supported in a multi-system way. - * 'key' should be set to a key which has already been set by + * 'key' should be set to a key which has already been set by * SET_VARIABLES. * 'data' will be set to a value or NULL. */ #define RETRO_ENVIRONMENT_SET_VARIABLES 16 /* const struct retro_variable * -- * Allows an implementation to signal the environment - * which variables it might want to check for later using + * which variables it might want to check for later using * GET_VARIABLE. - * This allows the frontend to present these variables to + * This allows the frontend to present these variables to * a user dynamically. - * This should be called as early as possible (ideally in - * retro_set_environment). + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterward it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. * - * 'data' points to an array of retro_variable structs + * 'data' points to an array of retro_variable structs * terminated by a { NULL, NULL } element. - * retro_variable::key should be namespaced to not collide - * with other implementations' keys. E.g. A core called + * retro_variable::key should be namespaced to not collide + * with other implementations' keys. E.g. A core called * 'foo' should use keys named as 'foo_option'. - * retro_variable::value should contain a human readable - * description of the key as well as a '|' delimited list + * retro_variable::value should contain a human readable + * description of the key as well as a '|' delimited list * of expected values. * - * The number of possible options should be very limited, - * i.e. it should be feasible to cycle through options + * The number of possible options should be very limited, + * i.e. it should be feasible to cycle through options * without a keyboard. * * First entry should be treated as a default. @@ -589,11 +638,11 @@ enum retro_mod * Example entry: * { "foo_option", "Speed hack coprocessor X; false|true" } * - * Text before first ';' is description. This ';' must be - * followed by a space, and followed by a list of possible + * Text before first ';' is description. This ';' must be + * followed by a space, and followed by a list of possible * values split up with '|'. * - * Only strings are operated on. The possible values will + * Only strings are operated on. The possible values will * generally be displayed and stored as-is by the frontend. */ #define RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE 17 @@ -604,72 +653,75 @@ enum retro_mod */ #define RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME 18 /* const bool * -- - * If true, the libretro implementation supports calls to + * If true, the libretro implementation supports calls to * retro_load_game() with NULL as argument. * Used by cores which can run without particular game data. * This should be called within retro_set_environment() only. */ #define RETRO_ENVIRONMENT_GET_LIBRETRO_PATH 19 /* const char ** -- - * Retrieves the absolute path from where this libretro + * Retrieves the absolute path from where this libretro * implementation was loaded. - * NULL is returned if the libretro was loaded statically - * (i.e. linked statically to frontend), or if the path cannot be + * NULL is returned if the libretro was loaded statically + * (i.e. linked statically to frontend), or if the path cannot be * determined. - * Mostly useful in cooperation with SET_SUPPORT_NO_GAME as assets can + * Mostly useful in cooperation with SET_SUPPORT_NO_GAME as assets can * be loaded without ugly hacks. */ - - /* Environment 20 was an obsolete version of SET_AUDIO_CALLBACK. + + /* Environment 20 was an obsolete version of SET_AUDIO_CALLBACK. * It was not used by any known core at the time, * and was removed from the API. */ +#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21 + /* const struct retro_frame_time_callback * -- + * Lets the core know how much time has passed since last + * invocation of retro_run(). + * The frontend can tamper with the timing to fake fast-forward, + * slow-motion, frame stepping, etc. + * In this case the delta time will use the reference value + * in frame_time_callback.. + */ #define RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK 22 /* const struct retro_audio_callback * -- - * Sets an interface which is used to notify a libretro core about audio + * Sets an interface which is used to notify a libretro core about audio * being available for writing. - * The callback can be called from any thread, so a core using this must + * The callback can be called from any thread, so a core using this must * have a thread safe audio implementation. - * It is intended for games where audio and video are completely + * It is intended for games where audio and video are completely * asynchronous and audio can be generated on the fly. - * This interface is not recommended for use with emulators which have + * This interface is not recommended for use with emulators which have * highly synchronous audio. * - * The callback only notifies about writability; the libretro core still + * The callback only notifies about writability; the libretro core still * has to call the normal audio callbacks - * to write audio. The audio callbacks must be called from within the + * to write audio. The audio callbacks must be called from within the * notification callback. * The amount of audio data to write is up to the implementation. * Generally, the audio callback will be called continously in a loop. * - * Due to thread safety guarantees and lack of sync between audio and - * video, a frontend can selectively disallow this interface based on - * internal configuration. A core using this interface must also + * Due to thread safety guarantees and lack of sync between audio and + * video, a frontend can selectively disallow this interface based on + * internal configuration. A core using this interface must also * implement the "normal" audio interface. * - * A libretro core using SET_AUDIO_CALLBACK should also make use of + * A libretro core using SET_AUDIO_CALLBACK should also make use of * SET_FRAME_TIME_CALLBACK. */ -#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21 - /* const struct retro_frame_time_callback * -- - * Lets the core know how much time has passed since last - * invocation of retro_run(). - * The frontend can tamper with the timing to fake fast-forward, - * slow-motion, frame stepping, etc. - * In this case the delta time will use the reference value - * in frame_time_callback.. - */ #define RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE 23 /* struct retro_rumble_interface * -- - * Gets an interface which is used by a libretro core to set + * Gets an interface which is used by a libretro core to set * state of rumble motors in controllers. - * A strong and weak motor is supported, and they can be + * 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 * -- - * Gets a bitmask telling which device type are expected to be + * Gets a bitmask telling which device type are expected to be * handled properly in a call to retro_input_state_t. - * Devices which are not handled or recognized always return + * Devices which are not handled or recognized always return * 0 in retro_input_state_t. * Example bitmask: caps = (1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG). * Should only be called in retro_run(). @@ -678,56 +730,56 @@ enum retro_mod /* struct retro_sensor_interface * -- * Gets access to the sensor interface. * The purpose of this interface is to allow - * setting state related to sensors such as polling rate, + * setting state related to sensors such as polling rate, * enabling/disable it entirely, etc. - * Reading sensor state is done via the normal + * Reading sensor state is done via the normal * input_state_callback API. */ #define RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE (26 | RETRO_ENVIRONMENT_EXPERIMENTAL) /* struct retro_camera_callback * -- * Gets an interface to a video camera driver. - * A libretro core can use this interface to get access to a + * A libretro core can use this interface to get access to a * video camera. - * New video frames are delivered in a callback in same + * New video frames are delivered in a callback in same * thread as retro_run(). * * GET_CAMERA_INTERFACE should be called in retro_load_game(). * - * Depending on the camera implementation used, camera frames + * Depending on the camera implementation used, camera frames * will be delivered as a raw framebuffer, * or as an OpenGL texture directly. * - * The core has to tell the frontend here which types of + * The core has to tell the frontend here which types of * buffers can be handled properly. - * An OpenGL texture can only be handled when using a + * An OpenGL texture can only be handled when using a * libretro GL core (SET_HW_RENDER). - * It is recommended to use a libretro GL core when + * It is recommended to use a libretro GL core when * using camera interface. * - * The camera is not started automatically. The retrieved start/stop + * The camera is not started automatically. The retrieved start/stop * functions must be used to explicitly * start and stop the camera driver. */ #define RETRO_ENVIRONMENT_GET_LOG_INTERFACE 27 /* struct retro_log_callback * -- - * Gets an interface for logging. This is useful for + * Gets an interface for logging. This is useful for * logging in a cross-platform way - * as certain platforms cannot use use stderr for logging. + * as certain platforms cannot use stderr for logging. * It also allows the frontend to * show logging information in a more suitable way. - * If this interface is not used, libretro cores should + * If this interface is not used, libretro cores should * log to stderr as desired. */ #define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28 /* struct retro_perf_callback * -- - * Gets an interface for performance counters. This is useful - * for performance logging in a cross-platform way and for detecting + * Gets an interface for performance counters. This is useful + * for performance logging in a cross-platform way and for detecting * architecture-specific features, such as SIMD support. */ #define RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE 29 /* struct retro_location_callback * -- * Gets access to the location interface. - * The purpose of this interface is to be able to retrieve + * The purpose of this interface is to be able to retrieve * location-based information from the host device, * such as current latitude / longitude. */ @@ -735,7 +787,7 @@ enum retro_mod #define RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY 30 /* const char ** -- * Returns the "core assets" directory of the frontend. - * This directory can be used to store specific assets that the + * This directory can be used to store specific assets that the * core relies upon, such as art assets, * input data, etc etc. * The returned value can be NULL. @@ -744,76 +796,77 @@ enum retro_mod */ #define RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY 31 /* const char ** -- - * Returns the "save" directory of the frontend. - * This directory can be used to store SRAM, memory cards, - * high scores, etc, if the libretro core + * Returns the "save" directory of the frontend, unless there is no + * save directory available. The save directory should be used to + * store SRAM, memory cards, high scores, etc, if the libretro core * cannot use the regular memory interface (retro_get_memory_data()). * - * NOTE: libretro cores used to check GET_SYSTEM_DIRECTORY for - * similar things before. - * They should still check GET_SYSTEM_DIRECTORY if they want to - * be backwards compatible. - * The path here can be NULL. It should only be non-NULL if the - * frontend user has set a specific save path. + * If the frontend cannot designate a save directory, it will return + * NULL to indicate that the core should attempt to operate without a + * save directory set. + * + * NOTE: early libretro cores used the system directory for save + * files. Cores that need to be backwards-compatible can still check + * GET_SYSTEM_DIRECTORY. */ #define RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO 32 /* const struct retro_system_av_info * -- - * Sets a new av_info structure. This can only be called from + * Sets a new av_info structure. This can only be called from * within retro_run(). - * This should *only* be used if the core is completely altering the + * This should *only* be used if the core is completely altering the * internal resolutions, aspect ratios, timings, sampling rate, etc. - * Calling this can require a full reinitialization of video/audio + * Calling this can require a full reinitialization of video/audio * drivers in the frontend, * - * so it is important to call it very sparingly, and usually only with + * 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 video and + * An eventual driver reinitialize will happen so that video and * audio callbacks - * happening after this call within the same retro_run() call will + * happening after this call within the same retro_run() call will * target the newly initialized driver. * - * This callback makes it possible to support configurable resolutions + * This callback makes it possible to support configurable resolutions * in games, which can be useful to * avoid setting the "worst case" in max_width/max_height. * - * ***HIGHLY RECOMMENDED*** Do not call this callback every time + * ***HIGHLY RECOMMENDED*** Do not call this callback every time * resolution changes in an emulator core if it's - * expected to be a temporary change, for the reasons of possible + * expected to be a temporary change, for the reasons of possible * driver reinitialization. - * This call is not a free pass for not trying to provide - * correct values in retro_get_system_av_info(). If you need to change - * things like aspect ratio or nominal width/height, - * use RETRO_ENVIRONMENT_SET_GEOMETRY, which is a softer variant + * This call is not a free pass for not trying to provide + * correct values in retro_get_system_av_info(). If you need to change + * things like aspect ratio or nominal width/height, + * use RETRO_ENVIRONMENT_SET_GEOMETRY, which is a softer variant * of SET_SYSTEM_AV_INFO. * - * If this returns false, the frontend does not acknowledge a + * If this returns false, the frontend does not acknowledge a * changed av_info struct. */ #define RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK 33 /* const struct retro_get_proc_address_interface * -- - * Allows a libretro core to announce support for the + * Allows a libretro core to announce support for the * get_proc_address() interface. - * This interface allows for a standard way to extend libretro where + * This interface allows for a standard way to extend libretro where * use of environment calls are too indirect, * e.g. for cases where the frontend wants to call directly into the core. * - * If a core wants to expose this interface, SET_PROC_ADDRESS_CALLBACK + * If a core wants to expose this interface, SET_PROC_ADDRESS_CALLBACK * **MUST** be called from within retro_set_environment(). */ #define RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO 34 /* const struct retro_subsystem_info * -- * This environment call introduces the concept of libretro "subsystems". - * A subsystem is a variant of a libretro core which supports + * A subsystem is a variant of a libretro core which supports * different kinds of games. - * The purpose of this is to support e.g. emulators which might + * The purpose of this is to support e.g. emulators which might * have special needs, e.g. Super Nintendo's Super GameBoy, Sufami Turbo. - * It can also be used to pick among subsystems in an explicit way + * It can also be used to pick among subsystems in an explicit way * if the libretro implementation is a multi-system emulator itself. * * Loading a game via a subsystem is done with retro_load_game_special(), - * and this environment call allows a libretro core to expose which + * and this environment call allows a libretro core to expose which * subsystems are supported for use with retro_load_game_special(). - * A core passes an array of retro_game_special_info which is terminated + * A core passes an array of retro_game_special_info which is terminated * with a zeroed out retro_game_special_info struct. * * If a core wants to use this functionality, SET_SUBSYSTEM_INFO @@ -821,68 +874,81 @@ enum retro_mod */ #define RETRO_ENVIRONMENT_SET_CONTROLLER_INFO 35 /* const struct retro_controller_info * -- - * This environment call lets a libretro core tell the frontend - * which controller types are recognized in calls to + * This environment call lets a libretro core tell the frontend + * which controller subclasses are recognized in calls to * retro_set_controller_port_device(). * - * Some emulators such as Super Nintendo - * support multiple lightgun types which must be specifically - * selected from. - * It is therefore sometimes necessary for a frontend to be able - * to tell the core about a special kind of input device which is - * not covered by the libretro input API. + * Some emulators such as Super Nintendo support multiple lightgun + * types which must be specifically selected from. It is therefore + * sometimes necessary for a frontend to be able to tell the core + * about a special kind of input device which is not specifcally + * provided by the Libretro API. * - * In order for a frontend to understand the workings of an input device, - * it must be a specialized type - * of the generic device types already defined in the libretro API. + * In order for a frontend to understand the workings of those devices, + * they must be defined as a specialized subclass of the generic device + * types already defined in the libretro API. * - * Which devices are supported can vary per input port. - * The core must pass an array of const struct retro_controller_info which - * is terminated with a blanked out struct. Each element of the struct - * corresponds to an ascending port index to - * retro_set_controller_port_device(). - * Even if special device types are set in the libretro core, + * The core must pass an array of const struct retro_controller_info which + * is terminated with a blanked out struct. Each element of the + * retro_controller_info struct corresponds to the ascending port index + * that is passed to retro_set_controller_port_device() when that function + * is called to indicate to the core that the frontend has changed the + * active device subclass. SEE ALSO: retro_set_controller_port_device() + * + * The ascending input port indexes provided by the core in the struct + * are generally presented by frontends as ascending User # or Player #, + * such as Player 1, Player 2, Player 3, etc. Which device subclasses are + * supported can vary per input port. + * + * The first inner element of each entry in the retro_controller_info array + * is a retro_controller_description struct that specifies the names and + * codes of all device subclasses that are available for the corresponding + * User or Player, beginning with the generic Libretro device that the + * subclasses are derived from. The second inner element of each entry is the + * total number of subclasses that are listed in the retro_controller_description. + * + * NOTE: Even if special device types are set in the libretro core, * libretro should only poll input based on the base input device types. */ #define RETRO_ENVIRONMENT_SET_MEMORY_MAPS (36 | RETRO_ENVIRONMENT_EXPERIMENTAL) /* const struct retro_memory_map * -- - * This environment call lets a libretro core tell the frontend + * This environment call lets a libretro core tell the frontend * about the memory maps this core emulates. * This can be used to implement, for example, cheats in a core-agnostic way. * - * Should only be used by emulators; it doesn't make much sense for + * Should only be used by emulators; it doesn't make much sense for * anything else. - * It is recommended to expose all relevant pointers through + * It is recommended to expose all relevant pointers through * retro_get_memory_* as well. * * Can be called from retro_init and retro_load_game. */ #define RETRO_ENVIRONMENT_SET_GEOMETRY 37 /* const struct retro_game_geometry * -- - * This environment call is similar to SET_SYSTEM_AV_INFO for changing - * video parameters, but provides a guarantee that drivers will not be + * This environment call is similar to SET_SYSTEM_AV_INFO for changing + * video parameters, but provides a guarantee that drivers will not be * reinitialized. * This can only be called from within retro_run(). * - * The purpose of this call is to allow a core to alter nominal - * width/heights as well as aspect ratios on-the-fly, which can be + * The purpose of this call is to allow a core to alter nominal + * width/heights as well as aspect ratios on-the-fly, which can be * useful for some emulators to change in run-time. * * max_width/max_height arguments are ignored and cannot be changed - * with this call as this could potentially require a reinitialization or a + * with this call as this could potentially require a reinitialization or a * non-constant time operation. * If max_width/max_height are to be changed, SET_SYSTEM_AV_INFO is required. * - * A frontend must guarantee that this environment call completes in + * A frontend must guarantee that this environment call completes in * constant time. */ -#define RETRO_ENVIRONMENT_GET_USERNAME 38 +#define RETRO_ENVIRONMENT_GET_USERNAME 38 /* const char ** * Returns the specified username of the frontend, if specified by the user. - * This username can be used as a nickname for a core that has online facilities + * This username can be used as a nickname for a core that has online facilities * or any other mode where personalization of the user is desirable. * The returned value can be NULL. - * If this environ callback is used by a core that requires a valid username, + * If this environ callback is used by a core that requires a valid username, * a default username should be specified by the core. */ #define RETRO_ENVIRONMENT_GET_LANGUAGE 39 @@ -920,20 +986,6 @@ enum retro_mod * A frontend must make sure that the pointer obtained from this function is * writeable (and readable). */ - -enum retro_hw_render_interface_type -{ - RETRO_HW_RENDER_INTERFACE_VULKAN = 0, - RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX -}; - -/* Base struct. All retro_hw_render_interface_* types - * contain at least these fields. */ -struct retro_hw_render_interface -{ - enum retro_hw_render_interface_type interface_type; - unsigned interface_version; -}; #define RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE (41 | RETRO_ENVIRONMENT_EXPERIMENTAL) /* const struct retro_hw_render_interface ** -- * Returns an API specific rendering interface for accessing API specific data. @@ -945,7 +997,6 @@ struct retro_hw_render_interface * Similarly, after context_destroyed callback returns, * the contents of the HW_RENDER_INTERFACE are invalidated. */ - #define RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS (42 | RETRO_ENVIRONMENT_EXPERIMENTAL) /* const bool * -- * If true, the libretro implementation supports achievements @@ -954,6 +1005,615 @@ struct retro_hw_render_interface * * This must be called before the first call to retro_run. */ +#define RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE (43 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const struct retro_hw_render_context_negotiation_interface * -- + * Sets an interface which lets the libretro core negotiate with frontend how a context is created. + * The semantics of this interface depends on which API is used in SET_HW_RENDER earlier. + * This interface will be used when the frontend is trying to create a HW rendering context, + * so it will be used after SET_HW_RENDER, but before the context_reset callback. + */ +#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 44 + /* uint64_t * -- + * Sets quirk flags associated with serialization. The frontend will zero any flags it doesn't + * recognize or support. Should be set in either retro_init or retro_load_game, but not both. + */ +#define RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT (44 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* N/A (null) * -- + * The frontend will try to use a 'shared' hardware context (mostly applicable + * to OpenGL) when a hardware context is being set up. + * + * Returns true if the frontend supports shared hardware contexts and false + * if the frontend does not support shared hardware contexts. + * + * This will do nothing on its own until SET_HW_RENDER env callbacks are + * being used. + */ +#define RETRO_ENVIRONMENT_GET_VFS_INTERFACE (45 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_vfs_interface_info * -- + * Gets access to the VFS interface. + * VFS presence needs to be queried prior to load_game or any + * get_system/save/other_directory being called to let front end know + * core supports VFS before it starts handing out paths. + * It is recomended to do so in retro_set_environment + */ +#define RETRO_ENVIRONMENT_GET_LED_INTERFACE (46 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_led_interface * -- + * Gets an interface which is used by a libretro core to set + * state of LEDs. + */ +#define RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE (47 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* int * -- + * Tells the core if the frontend wants audio or video. + * If disabled, the frontend will discard the audio or video, + * so the core may decide to skip generating a frame or generating audio. + * This is mainly used for increasing performance. + * Bit 0 (value 1): Enable Video + * Bit 1 (value 2): Enable Audio + * Bit 2 (value 4): Use Fast Savestates. + * Bit 3 (value 8): Hard Disable Audio + * Other bits are reserved for future use and will default to zero. + * If video is disabled: + * * The frontend wants the core to not generate any video, + * including presenting frames via hardware acceleration. + * * The frontend's video frame callback will do nothing. + * * After running the frame, the video output of the next frame should be + * no different than if video was enabled, and saving and loading state + * should have no issues. + * If audio is disabled: + * * The frontend wants the core to not generate any audio. + * * The frontend's audio callbacks will do nothing. + * * After running the frame, the audio output of the next frame should be + * no different than if audio was enabled, and saving and loading state + * should have no issues. + * Fast Savestates: + * * Guaranteed to be created by the same binary that will load them. + * * Will not be written to or read from the disk. + * * Suggest that the core assumes loading state will succeed. + * * Suggest that the core updates its memory buffers in-place if possible. + * * Suggest that the core skips clearing memory. + * * Suggest that the core skips resetting the system. + * * Suggest that the core may skip validation steps. + * Hard Disable Audio: + * * Used for a secondary core when running ahead. + * * Indicates that the frontend will never need audio from the core. + * * Suggests that the core may stop synthesizing audio, but this should not + * compromise emulation accuracy. + * * Audio output for the next frame does not matter, and the frontend will + * never need an accurate audio state in the future. + * * State will never be saved when using Hard Disable Audio. + */ +#define RETRO_ENVIRONMENT_GET_MIDI_INTERFACE (48 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_midi_interface ** -- + * Returns a MIDI interface that can be used for raw data I/O. + */ + +#define RETRO_ENVIRONMENT_GET_FASTFORWARDING (49 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* bool * -- + * Boolean value that indicates whether or not the frontend is in + * fastforwarding mode. + */ + +#define RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE (50 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* float * -- + * 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 + * refresh rate/framerate. + */ + +#define RETRO_ENVIRONMENT_GET_INPUT_BITMASKS (51 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* 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 + * grab all button states instead of multiple times. + * + * If it returns true, you can pass RETRO_DEVICE_ID_JOYPAD_MASK as 'id' + * to retro_input_state_t (make sure 'device' is set to RETRO_DEVICE_JOYPAD). + * It will return a bitmask of all the digital buttons. + */ + +#define RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION 52 + /* unsigned * -- + * Unsigned value is the API version number of the core options + * interface supported by the frontend. If callback return false, + * API version is assumed to be 0. + * + * In legacy code, core options are set by passing an array of + * retro_variable structs to RETRO_ENVIRONMENT_SET_VARIABLES. + * 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 + * 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. + * This allows the core to additionally set option sublabel information + * and/or provide localisation support. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS 53 + /* const struct retro_core_option_definition ** -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * 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). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * 'data' points to an array of retro_core_option_definition structs + * terminated by a { NULL, NULL, NULL, {{0}}, NULL } element. + * retro_core_option_definition::key should be namespaced to not collide + * with other implementations' keys. e.g. A core called + * 'foo' should use keys named as 'foo_option'. + * retro_core_option_definition::desc should contain a human readable + * description of the key. + * retro_core_option_definition::info should contain any additional human + * readable information text that a typical user may need to + * understand the functionality of the option. + * retro_core_option_definition::values is an array of retro_core_option_value + * structs terminated by a { NULL, NULL } element. + * > retro_core_option_definition::values[index].value is an expected option + * value. + * > retro_core_option_definition::values[index].label is a human readable + * label used when displaying the value on screen. If NULL, + * the value itself is used. + * retro_core_option_definition::default_value is the default core option + * setting. It must match one of the expected option values in the + * retro_core_option_definition::values array. If it does not, or the + * default value is NULL, the first entry in the + * retro_core_option_definition::values array is treated as the default. + * + * The number of possible options should be very limited, + * and must be less than RETRO_NUM_CORE_OPTION_VALUES_MAX. + * i.e. it should be feasible to cycle through options + * without a keyboard. + * + * Example entry: + * { + * "foo_option", + * "Speed hack coprocessor X", + * "Provides increased performance at the expense of reduced accuracy", + * { + * { "false", NULL }, + * { "true", NULL }, + * { "unstable", "Turbo (Unstable)" }, + * { NULL, NULL }, + * }, + * "false" + * } + * + * Only strings are operated on. The possible values will + * generally be displayed and stored as-is by the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL 54 + /* const struct retro_core_options_intl * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * 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). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * This is fundamentally the same as RETRO_ENVIRONMENT_SET_CORE_OPTIONS, + * with the addition of localisation support. The description of the + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS callback should be consulted + * for further details. + * + * 'data' points to a retro_core_options_intl struct. + * + * retro_core_options_intl::us is a pointer to an array of + * retro_core_option_definition structs defining the US English + * core options implementation. It must point to a valid array. + * + * retro_core_options_intl::local is a pointer to an array of + * retro_core_option_definition structs defining core options for + * the current frontend language. It may be NULL (in which case + * retro_core_options_intl::us is used by the frontend). Any items + * missing from this array will be read from retro_core_options_intl::us + * instead. + * + * NOTE: Default core option values are always taken from the + * retro_core_options_intl::us array. Any default values in + * retro_core_options_intl::local array will be ignored. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY 55 + /* struct retro_core_option_display * -- + * + * Allows an implementation to signal the environment to show + * or hide a variable when displaying core options. This is + * considered a *suggestion*. The frontend is free to ignore + * this callback, and its implementation not considered mandatory. + * + * 'data' points to a retro_core_option_display struct + * + * retro_core_option_display::key is a variable identifier + * which has already been set by SET_VARIABLES/SET_CORE_OPTIONS. + * + * retro_core_option_display::visible is a boolean, specifying + * whether variable should be displayed + * + * Note that all core option variables will be set visible by + * 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: + * File paths passed as parameters when using this API shall be well formed UNIX-style, + * using "/" (unquoted forward slash) as directory separator regardless of the platform's native separator. + * Paths shall also include at least one forward slash ("game.bin" is an invalid path, use "./game.bin" instead). + * Other than the directory separator, cores shall not make assumptions about path format: + * "C:/path/game.bin", "http://example.com/game.bin", "#game/game.bin", "./game.bin" (without quotes) are all valid paths. + * Cores may replace the basename or remove path components from the end, and/or add new components; + * however, cores shall not append "./", "../" or multiple consecutive forward slashes ("//") to paths they request to front end. + * The frontend is encouraged to make such paths work as well as it can, but is allowed to give up if the core alters paths too much. + * Frontends are encouraged, but not required, to support native file system paths (modulo replacing the directory separator, if applicable). + * Cores are allowed to try using them, but must remain functional if the front rejects such requests. + * Cores are encouraged to use the libretro-common filestream functions for file I/O, + * as they seamlessly integrate with VFS, deal with directory separator replacement as appropriate + * and provide platform-specific fallbacks in cases where front ends do not support VFS. */ + +/* Opaque file handle + * Introduced in VFS API v1 */ +struct retro_vfs_file_handle; + +/* Opaque directory handle + * Introduced in VFS API v3 */ +struct retro_vfs_dir_handle; + +/* File open flags + * Introduced in VFS API v1 */ +#define RETRO_VFS_FILE_ACCESS_READ (1 << 0) /* Read only mode */ +#define RETRO_VFS_FILE_ACCESS_WRITE (1 << 1) /* Write only mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified */ +#define RETRO_VFS_FILE_ACCESS_READ_WRITE (RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE) /* Read-write mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified*/ +#define RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING (1 << 2) /* Prevents discarding content of existing files opened for writing */ + +/* These are only hints. The frontend may choose to ignore them. Other than RAM/CPU/etc use, + and how they react to unlikely external interference (for example someone else writing to that file, + or the file's server going down), behavior will not change. */ +#define RETRO_VFS_FILE_ACCESS_HINT_NONE (0) +/* Indicate that the file will be accessed many times. The frontend should aggressively cache everything. */ +#define RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS (1 << 0) + +/* Seek positions */ +#define RETRO_VFS_SEEK_POSITION_START 0 +#define RETRO_VFS_SEEK_POSITION_CURRENT 1 +#define RETRO_VFS_SEEK_POSITION_END 2 + +/* stat() result flags + * Introduced in VFS API v3 */ +#define RETRO_VFS_STAT_IS_VALID (1 << 0) +#define RETRO_VFS_STAT_IS_DIRECTORY (1 << 1) +#define RETRO_VFS_STAT_IS_CHARACTER_SPECIAL (1 << 2) + +/* Get path from opaque handle. Returns the exact same path passed to file_open when getting the handle + * Introduced in VFS API v1 */ +typedef const char *(RETRO_CALLCONV *retro_vfs_get_path_t)(struct retro_vfs_file_handle *stream); + +/* Open a file for reading or writing. If path points to a directory, this will + * fail. Returns the opaque file handle, or NULL for error. + * Introduced in VFS API v1 */ +typedef struct retro_vfs_file_handle *(RETRO_CALLCONV *retro_vfs_open_t)(const char *path, unsigned mode, unsigned hints); + +/* Close the file and release its resources. Must be called if open_file returns non-NULL. Returns 0 on success, -1 on failure. + * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used. + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_close_t)(struct retro_vfs_file_handle *stream); + +/* Return the size of the file in bytes, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_size_t)(struct retro_vfs_file_handle *stream); + +/* Truncate file to specified size. Returns 0 on success or -1 on error + * Introduced in VFS API v2 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_truncate_t)(struct retro_vfs_file_handle *stream, int64_t length); + +/* Get the current read / write position for the file. Returns -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_tell_t)(struct retro_vfs_file_handle *stream); + +/* Set the current read/write position for the file. Returns the new position, -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_seek_t)(struct retro_vfs_file_handle *stream, int64_t offset, int seek_position); + +/* Read data from a file. Returns the number of bytes read, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_read_t)(struct retro_vfs_file_handle *stream, void *s, uint64_t len); + +/* Write data to a file. Returns the number of bytes written, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_write_t)(struct retro_vfs_file_handle *stream, const void *s, uint64_t len); + +/* Flush pending writes to file, if using buffered IO. Returns 0 on sucess, or -1 on failure. + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_flush_t)(struct retro_vfs_file_handle *stream); + +/* Delete the specified file. Returns 0 on success, -1 on failure + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_remove_t)(const char *path); + +/* Rename the specified file. Returns 0 on success, -1 on failure + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_rename_t)(const char *old_path, const char *new_path); + +/* Stat the specified file. Retruns a bitmask of RETRO_VFS_STAT_* flags, none are set if path was not valid. + * Additionally stores file size in given variable, unless NULL is given. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size); + +/* Create the specified directory. Returns 0 on success, -1 on unknown failure, -2 if already exists. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_mkdir_t)(const char *dir); + +/* Open the specified directory for listing. Returns the opaque dir handle, or NULL for error. + * Support for the include_hidden argument may vary depending on the platform. + * Introduced in VFS API v3 */ +typedef struct retro_vfs_dir_handle *(RETRO_CALLCONV *retro_vfs_opendir_t)(const char *dir, bool include_hidden); + +/* Read the directory entry at the current position, and move the read pointer to the next position. + * Returns true on success, false if already on the last entry. + * Introduced in VFS API v3 */ +typedef bool (RETRO_CALLCONV *retro_vfs_readdir_t)(struct retro_vfs_dir_handle *dirstream); + +/* Get the name of the last entry read. Returns a string on success, or NULL for error. + * The returned string pointer is valid until the next call to readdir or closedir. + * Introduced in VFS API v3 */ +typedef const char *(RETRO_CALLCONV *retro_vfs_dirent_get_name_t)(struct retro_vfs_dir_handle *dirstream); + +/* Check if the last entry read was a directory. Returns true if it was, false otherwise (or on error). + * Introduced in VFS API v3 */ +typedef bool (RETRO_CALLCONV *retro_vfs_dirent_is_dir_t)(struct retro_vfs_dir_handle *dirstream); + +/* Close the directory and release its resources. Must be called if opendir returns non-NULL. Returns 0 on success, -1 on failure. + * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_closedir_t)(struct retro_vfs_dir_handle *dirstream); + +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; +}; + +struct retro_vfs_interface_info +{ + /* Set by core: should this be higher than the version the front end supports, + * front end will return false in the RETRO_ENVIRONMENT_GET_VFS_INTERFACE call + * Introduced in VFS API v1 */ + uint32_t required_interface_version; + + /* Frontend writes interface pointer here. The frontend also sets the actual + * version, must be at least required_interface_version. + * Introduced in VFS API v1 */ + struct retro_vfs_interface *iface; +}; + +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 +}; + +/* Base struct. All retro_hw_render_interface_* types + * contain at least these fields. */ +struct retro_hw_render_interface +{ + enum retro_hw_render_interface_type interface_type; + unsigned interface_version; +}; + +typedef void (RETRO_CALLCONV *retro_set_led_state_t)(int led, int state); +struct retro_led_interface +{ + retro_set_led_state_t set_led_state; +}; + +/* Retrieves the current state of the MIDI input. + * Returns true if it's enabled, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_input_enabled_t)(void); + +/* Retrieves the current state of the MIDI output. + * Returns true if it's enabled, false otherwise */ +typedef bool (RETRO_CALLCONV *retro_midi_output_enabled_t)(void); + +/* Reads next byte from the input stream. + * Returns true if byte is read, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_read_t)(uint8_t *byte); + +/* Writes byte to the output stream. + * 'delta_time' is in microseconds and represent time elapsed since previous write. + * Returns true if byte is written, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_write_t)(uint8_t byte, uint32_t delta_time); + +/* Flushes previously written data. + * Returns true if successful, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_flush_t)(void); + +struct retro_midi_interface +{ + retro_midi_input_enabled_t input_enabled; + retro_midi_output_enabled_t output_enabled; + retro_midi_read_t read; + retro_midi_write_t write; + retro_midi_flush_t flush; +}; enum retro_hw_render_context_negotiation_interface_type { @@ -968,77 +1628,97 @@ struct retro_hw_render_context_negotiation_interface enum retro_hw_render_context_negotiation_interface_type interface_type; unsigned interface_version; }; -#define RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE (43 | RETRO_ENVIRONMENT_EXPERIMENTAL) - /* const struct retro_hw_render_context_negotiation_interface * -- - * Sets an interface which lets the libretro core negotiate with frontend how a context is created. - * The semantics of this interface depends on which API is used in SET_HW_RENDER earlier. - * This interface will be used when the frontend is trying to create a HW rendering context, - * so it will be used after SET_HW_RENDER, but before the context_reset callback. - */ -#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */ -#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */ -#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */ -#define RETRO_MEMDESC_ALIGN_4 (2 << 16) -#define RETRO_MEMDESC_ALIGN_8 (3 << 16) -#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */ -#define RETRO_MEMDESC_MINSIZE_4 (2 << 24) -#define RETRO_MEMDESC_MINSIZE_8 (3 << 24) +/* Serialized state is incomplete in some way. Set if serialization is + * usable in typical end-user cases but should not be relied upon to + * implement frame-sensitive frontend features such as netplay or + * rerecording. */ +#define RETRO_SERIALIZATION_QUIRK_INCOMPLETE (1 << 0) +/* The core must spend some time initializing before serialization is + * supported. retro_serialize() will initially fail; retro_unserialize() + * and retro_serialize_size() may or may not work correctly either. */ +#define RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE (1 << 1) +/* Serialization size may change within a session. */ +#define RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE (1 << 2) +/* Set by the frontend to acknowledge that it supports variable-sized + * states. */ +#define RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE (1 << 3) +/* Serialized state can only be loaded during the same session. */ +#define RETRO_SERIALIZATION_QUIRK_SINGLE_SESSION (1 << 4) +/* Serialized state cannot be loaded on an architecture with a different + * endianness from the one it was saved on. */ +#define RETRO_SERIALIZATION_QUIRK_ENDIAN_DEPENDENT (1 << 5) +/* Serialized state cannot be loaded on a different platform from the one it + * was saved on for reasons other than endianness, such as word size + * dependence */ +#define RETRO_SERIALIZATION_QUIRK_PLATFORM_DEPENDENT (1 << 6) + +#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */ +#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */ +#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2) /* The memory area is system RAM. This is main RAM of the gaming system. */ +#define RETRO_MEMDESC_SAVE_RAM (1 << 3) /* The memory area is save RAM. This RAM is usually found on a game cartridge, backed up by a battery. */ +#define RETRO_MEMDESC_VIDEO_RAM (1 << 4) /* The memory area is video RAM (VRAM) */ +#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */ +#define RETRO_MEMDESC_ALIGN_4 (2 << 16) +#define RETRO_MEMDESC_ALIGN_8 (3 << 16) +#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */ +#define RETRO_MEMDESC_MINSIZE_4 (2 << 24) +#define RETRO_MEMDESC_MINSIZE_8 (3 << 24) struct retro_memory_descriptor { uint64_t flags; /* Pointer to the start of the relevant ROM or RAM chip. - * It's strongly recommended to use 'offset' if possible, rather than + * It's strongly recommended to use 'offset' if possible, rather than * doing math on the pointer. * - * If the same byte is mapped my multiple descriptors, their descriptors + * If the same byte is mapped my multiple descriptors, their descriptors * must have the same pointer. - * If 'start' does not point to the first byte in the pointer, put the + * If 'start' does not point to the first byte in the pointer, put the * difference in 'offset' instead. * - * May be NULL if there's nothing usable here (e.g. hardware registers and + * May be NULL if there's nothing usable here (e.g. hardware registers and * open bus). No flags should be set if the pointer is NULL. * It's recommended to minimize the number of descriptors if possible, * but not mandatory. */ void *ptr; size_t offset; - /* This is the location in the emulated address space + /* This is the location in the emulated address space * where the mapping starts. */ size_t start; /* Which bits must be same as in 'start' for this mapping to apply. - * The first memory descriptor to claim a certain byte is the one + * The first memory descriptor to claim a certain byte is the one * that applies. * A bit which is set in 'start' must also be set in this. - * Can be zero, in which case each byte is assumed mapped exactly once. + * Can be zero, in which case each byte is assumed mapped exactly once. * In this case, 'len' must be a power of two. */ size_t select; - /* If this is nonzero, the set bits are assumed not connected to the + /* If this is nonzero, the set bits are assumed not connected to the * memory chip's address pins. */ size_t disconnect; /* This one tells the size of the current memory area. - * If, after start+disconnect are applied, the address is higher than + * If, after start+disconnect are applied, the address is higher than * this, the highest bit of the address is cleared. * * If the address is still too high, the next highest bit is cleared. - * Can be zero, in which case it's assumed to be infinite (as limited + * Can be zero, in which case it's assumed to be infinite (as limited * by 'select' and 'disconnect'). */ size_t len; - /* To go from emulated address to physical address, the following + /* To go from emulated address to physical address, the following * order applies: * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */ - /* The address space name must consist of only a-zA-Z0-9_-, + /* The address space name must consist of only a-zA-Z0-9_-, * should be as short as feasible (maximum length is 8 plus the NUL), - * and may not be any other address space plus one or more 0-9A-F + * and may not be any other address space plus one or more 0-9A-F * at the end. - * However, multiple memory descriptors for the same address space is - * allowed, and the address space name can be empty. NULL is treated + * However, multiple memory descriptors for the same address space is + * allowed, and the address space name can be empty. NULL is treated * as empty. * * Address space names are case sensitive, but avoid lowercase if possible. @@ -1052,11 +1732,11 @@ struct retro_memory_descriptor * 'a'+blank - valid ('a' is not in 0-9A-F) * 'a'+'A' - valid (neither is a prefix of each other) * 'AR'+blank - valid ('R' is not in 0-9A-F) - * 'ARB'+blank - valid (the B can't be part of the address either, because + * 'ARB'+blank - valid (the B can't be part of the address either, because * there is no namespace 'AR') - * blank+'B' - not valid, because it's ambigous which address space B1234 + * blank+'B' - not valid, because it's ambigous which address space B1234 * would refer to. - * The length can't be used for that purpose; the frontend may want + * The length can't be used for that purpose; the frontend may want * to append arbitrary data to an address, without a separator. */ const char *addrspace; @@ -1078,32 +1758,32 @@ struct retro_memory_descriptor * the most recent addition and continue on the next bit. * TODO: Can the above be optimized? Is "remove the lowest bit set in both * pointer and 'len'" equivalent? */ - + /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing * the emulated memory in 32-bit chunks, native endian. But that's nothing * compared to Darek Mihocka * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE * RAM backwards! I'll want to represent both of those, via some flags. - * + * * I suspect MAME either didn't think of that idea, or don't want the #ifdef. * Not sure which, nor do I really care. */ - + /* TODO: Some of those flags are unused and/or don't really make sense. Clean * them up. */ }; -/* The frontend may use the largest value of 'start'+'select' in a +/* The frontend may use the largest value of 'start'+'select' in a * certain namespace to infer the size of the address space. * - * If the address space is larger than that, a mapping with .ptr=NULL - * should be at the end of the array, with .select set to all ones for + * If the address space is larger than that, a mapping with .ptr=NULL + * should be at the end of the array, with .select set to all ones for * as long as the address space is big. * * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags): * SNES WRAM: * .start=0x7E0000, .len=0x20000 - * (Note that this must be mapped before the ROM in most cases; some of the - * ROM mappers + * (Note that this must be mapped before the ROM in most cases; some of the + * ROM mappers * try to claim $7E0000, or at least $7E8000.) * SNES SPC700 RAM: * .addrspace="S", .len=0x10000 @@ -1112,7 +1792,7 @@ struct retro_memory_descriptor * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000 * SNES WRAM mirrors, alternate equivalent descriptor: * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF - * (Various similar constructions can be created by combining parts of + * (Various similar constructions can be created by combining parts of * the above two.) * SNES LoROM (512KB, mirrored a couple of times): * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024 @@ -1138,13 +1818,13 @@ struct retro_memory_map struct retro_controller_description { - /* Human-readable description of the controller. Even if using a generic - * input device type, this can be set to the particular device type the + /* Human-readable description of the controller. Even if using a generic + * input device type, this can be set to the particular device type the * core uses. */ const char *desc; - /* Device type passed to retro_set_controller_port_device(). If the device - * type is a sub-class of a generic input device type, use the + /* Device type passed to retro_set_controller_port_device(). If the device + * type is a sub-class of a generic input device type, use the * RETRO_DEVICE_SUBCLASS macro to create an ID. * * E.g. RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 1). */ @@ -1162,8 +1842,8 @@ struct retro_subsystem_memory_info /* The extension associated with a memory type, e.g. "psram". */ const char *extension; - /* The memory type for retro_get_memory(). This should be at - * least 0x100 to avoid conflict with standardized + /* The memory type for retro_get_memory(). This should be at + * least 0x100 to avoid conflict with standardized * libretro memory types. */ unsigned type; }; @@ -1182,11 +1862,11 @@ struct retro_subsystem_rom_info /* Same definition as retro_get_system_info(). */ bool block_extract; - /* This is set if the content is required to load a game. + /* This is set if the content is required to load a game. * If this is set to false, a zeroed-out retro_game_info can be passed. */ bool required; - /* Content can have multiple associated persistent + /* Content can have multiple associated persistent * memory types (retro_get_memory()). */ const struct retro_subsystem_memory_info *memory; unsigned num_memory; @@ -1204,17 +1884,17 @@ struct retro_subsystem_info */ const char *ident; - /* Infos for each content file. The first entry is assumed to be the + /* Infos for each content file. The first entry is assumed to be the * "most significant" content for frontend purposes. - * E.g. with Super GameBoy, the first content should be the GameBoy ROM, + * E.g. with Super GameBoy, the first content should be the GameBoy ROM, * as it is the most "significant" content to a user. - * If a frontend creates new file paths based on the content used + * If a frontend creates new file paths based on the content used * (e.g. savestates), it should use the path for the first ROM to do so. */ const struct retro_subsystem_rom_info *roms; /* Number of content files associated with a subsystem. */ unsigned num_roms; - + /* The type passed to retro_load_game_special(). */ unsigned id; }; @@ -1225,13 +1905,13 @@ typedef void (RETRO_CALLCONV *retro_proc_address_t)(void); * (None here so far). * * Get a symbol from a libretro core. - * Cores should only return symbols which are actual + * Cores should only return symbols which are actual * extensions to the libretro API. * - * Frontends should not use this to obtain symbols to standard + * Frontends should not use this to obtain symbols to standard * libretro entry points (static linking or dlsym). * - * The symbol name must be equal to the function name, + * The symbol name must be equal to the function name, * e.g. if void retro_foo(void); exists, the symbol must be called "retro_foo". * The returned function pointer must be cast to the corresponding type. */ @@ -1285,6 +1965,7 @@ struct retro_log_callback #define RETRO_SIMD_POPCNT (1 << 18) #define RETRO_SIMD_MOVBE (1 << 19) #define RETRO_SIMD_CMOV (1 << 20) +#define RETRO_SIMD_ASIMD (1 << 21) typedef uint64_t retro_perf_tick_t; typedef int64_t retro_time_t; @@ -1305,7 +1986,7 @@ struct retro_perf_counter typedef retro_time_t (RETRO_CALLCONV *retro_perf_get_time_usec_t)(void); /* A simple counter. Usually nanoseconds, but can also be CPU cycles. - * Can be used directly if desired (when creating a more sophisticated + * Can be used directly if desired (when creating a more sophisticated * performance counter system). * */ typedef retro_perf_tick_t (RETRO_CALLCONV *retro_perf_get_counter_t)(void); @@ -1319,9 +2000,9 @@ typedef uint64_t (RETRO_CALLCONV *retro_get_cpu_features_t)(void); typedef void (RETRO_CALLCONV *retro_perf_log_t)(void); /* Register a performance counter. - * ident field must be set with a discrete value and other values in + * ident field must be set with a discrete value and other values in * retro_perf_counter must be 0. - * Registering can be called multiple times. To avoid calling to + * Registering can be called multiple times. To avoid calling to * frontend redundantly, you can check registered field first. */ typedef void (RETRO_CALLCONV *retro_perf_register_t)(struct retro_perf_counter *counter); @@ -1383,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 }; @@ -1391,8 +2076,12 @@ 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, +typedef bool (RETRO_CALLCONV *retro_set_sensor_state_t)(unsigned port, enum retro_sensor_action action, unsigned rate); typedef float (RETRO_CALLCONV *retro_sensor_get_input_t)(unsigned port, unsigned id); @@ -1417,7 +2106,7 @@ typedef bool (RETRO_CALLCONV *retro_camera_start_t)(void); /* Stops the camera driver. Can only be called in retro_run(). */ typedef void (RETRO_CALLCONV *retro_camera_stop_t)(void); -/* Callback which signals when the camera driver is initialized +/* Callback which signals when the camera driver is initialized * and/or deinitialized. * retro_camera_start_t can be called in initialized callback. */ @@ -1427,36 +2116,36 @@ typedef void (RETRO_CALLCONV *retro_camera_lifetime_status_t)(void); * Width, height and pitch are similar to retro_video_refresh_t. * First pixel is top-left origin. */ -typedef void (RETRO_CALLCONV *retro_camera_frame_raw_framebuffer_t)(const uint32_t *buffer, +typedef void (RETRO_CALLCONV *retro_camera_frame_raw_framebuffer_t)(const uint32_t *buffer, unsigned width, unsigned height, size_t pitch); /* A callback for when OpenGL textures are used. * * texture_id is a texture owned by camera driver. - * Its state or content should be considered immutable, except for things like + * Its state or content should be considered immutable, except for things like * texture filtering and clamping. * * texture_target is the texture target for the GL texture. - * These can include e.g. GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE, and possibly + * These can include e.g. GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE, and possibly * more depending on extensions. * - * affine points to a packed 3x3 column-major matrix used to apply an affine + * affine points to a packed 3x3 column-major matrix used to apply an affine * transform to texture coordinates. (affine_matrix * vec3(coord_x, coord_y, 1.0)) - * After transform, normalized texture coord (0, 0) should be bottom-left + * After transform, normalized texture coord (0, 0) should be bottom-left * and (1, 1) should be top-right (or (width, height) for RECTANGLE). * - * GL-specific typedefs are avoided here to avoid relying on gl.h in + * GL-specific typedefs are avoided here to avoid relying on gl.h in * the API definition. */ -typedef void (RETRO_CALLCONV *retro_camera_frame_opengl_texture_t)(unsigned texture_id, +typedef void (RETRO_CALLCONV *retro_camera_frame_opengl_texture_t)(unsigned texture_id, unsigned texture_target, const float *affine); struct retro_camera_callback { - /* Set by libretro core. + /* Set by libretro core. * Example bitmask: caps = (1 << RETRO_CAMERA_BUFFER_OPENGL_TEXTURE) | (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER). */ - uint64_t caps; + uint64_t caps; /* Desired resolution for camera. Is only used as a hint. */ unsigned width; @@ -1470,22 +2159,22 @@ struct retro_camera_callback retro_camera_frame_raw_framebuffer_t frame_raw_framebuffer; /* Set by libretro core if OpenGL texture callbacks will be used. */ - retro_camera_frame_opengl_texture_t frame_opengl_texture; + retro_camera_frame_opengl_texture_t frame_opengl_texture; - /* Set by libretro core. Called after camera driver is initialized and + /* Set by libretro core. Called after camera driver is initialized and * ready to be started. * Can be NULL, in which this callback is not called. */ retro_camera_lifetime_status_t initialized; - /* Set by libretro core. Called right before camera driver is + /* Set by libretro core. Called right before camera driver is * deinitialized. * Can be NULL, in which this callback is not called. */ retro_camera_lifetime_status_t deinitialized; }; -/* Sets the interval of time and/or distance at which to update/poll +/* Sets the interval of time and/or distance at which to update/poll * location-based data. * * To ensure compatibility with all location-based implementations, @@ -1498,20 +2187,20 @@ typedef void (RETRO_CALLCONV *retro_location_set_interval_t)(unsigned interval_m unsigned interval_distance); /* Start location services. The device will start listening for changes to the - * current location at regular intervals (which are defined with + * current location at regular intervals (which are defined with * retro_location_set_interval_t). */ typedef bool (RETRO_CALLCONV *retro_location_start_t)(void); -/* Stop location services. The device will stop listening for changes +/* Stop location services. The device will stop listening for changes * to the current location. */ typedef void (RETRO_CALLCONV *retro_location_stop_t)(void); -/* Get the position of the current location. Will set parameters to +/* Get the position of the current location. Will set parameters to * 0 if no new location update has happened since the last time. */ typedef bool (RETRO_CALLCONV *retro_location_get_position_t)(double *lat, double *lon, double *horiz_accuracy, double *vert_accuracy); -/* Callback which signals when the location driver is initialized +/* Callback which signals when the location driver is initialized * and/or deinitialized. * retro_location_start_t can be called in initialized callback. */ @@ -1536,14 +2225,14 @@ enum retro_rumble_effect RETRO_RUMBLE_DUMMY = INT_MAX }; -/* Sets rumble state for joypad plugged in port 'port'. +/* Sets rumble state for joypad plugged in port 'port'. * Rumble effects are controlled independently, * and setting e.g. strong rumble does not override weak rumble. * Strength has a range of [0, 0xffff]. * - * Returns true if rumble state request was honored. + * Returns true if rumble state request was honored. * Calling this before first retro_run() is likely to return false. */ -typedef bool (RETRO_CALLCONV *retro_set_rumble_state_t)(unsigned port, +typedef bool (RETRO_CALLCONV *retro_set_rumble_state_t)(unsigned port, enum retro_rumble_effect effect, uint16_t strength); struct retro_rumble_interface @@ -1554,10 +2243,10 @@ struct retro_rumble_interface /* Notifies libretro that audio data should be written. */ typedef void (RETRO_CALLCONV *retro_audio_callback_t)(void); -/* True: Audio driver in frontend is active, and callback is +/* True: Audio driver in frontend is active, and callback is * expected to be called regularily. - * False: Audio driver in frontend is paused or inactive. - * Audio callback will not be called until set_state has been + * False: Audio driver in frontend is paused or inactive. + * Audio callback will not be called until set_state has been * called with true. * Initial state is false (inactive). */ @@ -1569,11 +2258,11 @@ struct retro_audio_callback retro_audio_set_state_callback_t set_state; }; -/* Notifies a libretro core of time spent since last invocation +/* Notifies a libretro core of time spent since last invocation * of retro_run() in microseconds. * * It will be called right before retro_run() every frame. - * The frontend can tamper with timing to support cases like + * The frontend can tamper with timing to support cases like * fast-forward, slow-motion and framestepping. * * In those scenarios the reference frame time value will be used. */ @@ -1582,12 +2271,36 @@ typedef void (RETRO_CALLCONV *retro_frame_time_callback_t)(retro_usec_t usec); struct retro_frame_time_callback { retro_frame_time_callback_t callback; - /* Represents the time of one frame. It is computed as - * 1000000 / fps, but the implementation will resolve the + /* Represents the time of one frame. It is computed as + * 1000000 / fps, but the implementation will resolve the * rounding to ensure that framestepping, etc is exact. */ 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. * */ @@ -1599,7 +2312,7 @@ struct retro_frame_time_callback * it should implement context_destroy callback. * If called, all GPU resources must be reinitialized. * Usually called when frontend reinits video driver. - * Also called first time video driver is initialized, + * Also called first time video driver is initialized, * allowing libretro core to initialize resources. */ typedef void (RETRO_CALLCONV *retro_hw_context_reset_t)(void); @@ -1616,7 +2329,7 @@ enum retro_hw_context_type { RETRO_HW_CONTEXT_NONE = 0, /* OpenGL 2.x. Driver can choose to use latest compatibility context. */ - RETRO_HW_CONTEXT_OPENGL = 1, + RETRO_HW_CONTEXT_OPENGL = 1, /* OpenGL ES 2.0. */ RETRO_HW_CONTEXT_OPENGLES2 = 2, /* Modern desktop core GL context. Use version_major/ @@ -1631,6 +2344,10 @@ enum retro_hw_context_type /* Vulkan, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE. */ RETRO_HW_CONTEXT_VULKAN = 6, + /* Direct3D, set version_major to select the type of interface + * returned by RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */ + RETRO_HW_CONTEXT_DIRECT3D = 7, + RETRO_HW_CONTEXT_DUMMY = INT_MAX }; @@ -1642,10 +2359,10 @@ struct retro_hw_render_callback /* Called when a context has been created or when it has been reset. * An OpenGL context is only valid after context_reset() has been called. * - * When context_reset is called, OpenGL resources in the libretro + * When context_reset is called, OpenGL resources in the libretro * implementation are guaranteed to be invalid. * - * It is possible that context_reset is called multiple times during an + * It is possible that context_reset is called multiple times during an * application lifecycle. * If context_reset is called without any notification (context_destroy), * the OpenGL context was lost and resources should just be recreated @@ -1658,7 +2375,8 @@ struct retro_hw_render_callback * be providing preallocated framebuffers. */ retro_hw_get_current_framebuffer_t get_current_framebuffer; - /* Set by frontend. */ + /* Set by frontend. + * Can return all relevant functions, including glClear on Windows. */ retro_hw_get_proc_address_t get_proc_address; /* Set if render buffers should have depth component attached. @@ -1669,48 +2387,48 @@ struct retro_hw_render_callback * TODO: Obsolete. */ bool stencil; - /* If depth and stencil are true, a packed 24/8 buffer will be added. + /* If depth and stencil are true, a packed 24/8 buffer will be added. * Only attaching stencil is invalid and will be ignored. */ - /* Use conventional bottom-left origin convention. If false, + /* Use conventional bottom-left origin convention. If false, * standard libretro top-left origin semantics are used. * TODO: Move to GL specific interface. */ bool bottom_left_origin; - + /* Major version number for core GL context or GLES 3.1+. */ unsigned version_major; /* Minor version number for core GL context or GLES 3.1+. */ unsigned version_minor; - /* If this is true, the frontend will go very far to avoid + /* If this is true, the frontend will go very far to avoid * resetting context in scenarios like toggling fullscreen, etc. * TODO: Obsolete? Maybe frontend should just always assume this ... */ bool cache_context; - /* The reset callback might still be called in extreme situations + /* The reset callback might still be called in extreme situations * such as if the context is lost beyond recovery. * - * For optimal stability, set this to false, and allow context to be + * For optimal stability, set this to false, and allow context to be * reset at any time. */ - - /* A callback to be called before the context is destroyed in a + + /* A callback to be called before the context is destroyed in a * controlled way by the frontend. */ retro_hw_context_reset_t context_destroy; /* OpenGL resources can be deinitialized cleanly at this step. - * context_destroy can be set to NULL, in which resources will + * context_destroy can be set to NULL, in which resources will * just be destroyed without any notification. * - * Even when context_destroy is non-NULL, it is possible that + * Even when context_destroy is non-NULL, it is possible that * context_reset is called without any destroy notification. - * This happens if context is lost by external factors (such as + * This happens if context is lost by external factors (such as * notified by GL_ARB_robustness). * * In this case, the context is assumed to be already dead, - * and the libretro implementation must not try to free any OpenGL + * and the libretro implementation must not try to free any OpenGL * resources in the subsequent context_reset. */ @@ -1718,7 +2436,7 @@ struct retro_hw_render_callback bool debug_context; }; -/* Callback type passed in RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK. +/* Callback type passed in RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK. * Called by the frontend in response to keyboard events. * down is set if the key is being pressed, or false if it is being released. * keycode is the RETROK value of the char. @@ -1726,16 +2444,16 @@ struct retro_hw_render_callback * key_modifiers is a set of RETROKMOD values or'ed together. * * The pressed/keycode state can be indepedent of the character. - * It is also possible that multiple characters are generated from a + * It is also possible that multiple characters are generated from a * single keypress. * Keycode events should be treated separately from character events. * However, when possible, the frontend should try to synchronize these. * If only a character is posted, keycode should be RETROK_UNKNOWN. * - * Similarily if only a keycode event is generated with no corresponding + * Similarily if only a keycode event is generated with no corresponding * character, character should be 0. */ -typedef void (RETRO_CALLCONV *retro_keyboard_event_t)(bool down, unsigned keycode, +typedef void (RETRO_CALLCONV *retro_keyboard_event_t)(bool down, unsigned keycode, uint32_t character, uint16_t key_modifiers); struct retro_keyboard_callback @@ -1743,16 +2461,17 @@ struct retro_keyboard_callback retro_keyboard_event_t callback; }; -/* Callbacks for RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE. - * Should be set for implementations which can swap out multiple disk +/* 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. * * If the implementation can do this automatically, it should strive to do so. * However, there are cases where the user must manually do so. * - * Overview: To swap a disk image, eject the disk image with + * Overview: To swap a disk image, eject the disk image with * set_eject_state(true). - * Set the disk index with set_image_index(index). Insert the disk again + * Set the disk index with set_image_index(index). Insert the disk again * with set_eject_state(false). */ @@ -1770,7 +2489,7 @@ typedef bool (RETRO_CALLCONV *retro_get_eject_state_t)(void); typedef unsigned (RETRO_CALLCONV *retro_get_image_index_t)(void); /* Sets image index. Can only be called when disk is ejected. - * The implementation supports setting "no disk" by using an + * The implementation supports setting "no disk" by using an * index >= get_num_images(). */ typedef bool (RETRO_CALLCONV *retro_set_image_index_t)(unsigned index); @@ -1784,11 +2503,11 @@ struct retro_game_info; * Arguments to pass in info have same requirements as retro_load_game(). * Virtual disk tray must be ejected when calling this. * - * Replacing a disk image with info = NULL will remove the disk image + * Replacing a disk image with info = NULL will remove the disk image * from the internal list. * As a result, calls to get_image_index() can change. * - * E.g. replace_image_index(1, NULL), and previous get_image_index() + * E.g. replace_image_index(1, NULL), and previous get_image_index() * returned 4 before. * Index 1 will be removed, and the new index is 3. */ @@ -1797,10 +2516,57 @@ typedef bool (RETRO_CALLCONV *retro_replace_image_index_t)(unsigned index, /* Adds a new valid index (get_num_images()) to the internal disk list. * This will increment subsequent return values from get_num_images() by 1. - * This image index cannot be used until a disk image has been set + * This image index cannot be used until a disk image has been set * 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; @@ -1814,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. @@ -1828,7 +2615,7 @@ enum retro_pixel_format /* RGB565, native endian. * This pixel format is the recommended format to use if a 15/16-bit - * format is desired as it is the pixel format that is typically + * format is desired as it is the pixel format that is typically * available on a wide range of low-power devices. * * It is also natively supported in APIs like OpenGL ES. */ @@ -1844,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. */ @@ -1858,43 +2743,52 @@ struct retro_input_descriptor /* Human readable description for parameters. * The pointer must remain valid until * retro_unload_game() is called. */ - const char *description; + const char *description; }; struct retro_system_info { - /* All pointers are owned by libretro implementation, and pointers must - * remain valid until retro_deinit() is called. */ + /* All pointers are owned by libretro implementation, and pointers must + * remain valid until it is unloaded. */ - const char *library_name; /* Descriptive name of library. Should not + const char *library_name; /* Descriptive name of library. Should not * contain any version numbers, etc. */ const char *library_version; /* Descriptive version of core. */ - const char *valid_extensions; /* A string listing probably content - * extensions the core will be able to + const char *valid_extensions; /* A string listing probably content + * extensions the core will be able to * load, separated with pipe. * I.e. "bin|rom|iso". - * Typically used for a GUI to filter + * Typically used for a GUI to filter * out extensions. */ - /* If true, retro_load_game() is guaranteed to provide a valid pathname - * in retro_game_info::path. - * ::data and ::size are both invalid. + /* Libretro cores that need to have direct access to their content + * files, including cores which use the path of the content files to + * determine the paths of other files, should set need_fullpath to true. * - * If false, ::data and ::size are guaranteed to be valid, but ::path - * might not be valid. + * Cores should strive for setting need_fullpath to false, + * as it allows the frontend to perform patching, etc. * - * This is typically set to true for libretro implementations that must - * load from file. - * Implementations should strive for setting this to false, as it allows - * the frontend to perform patching, etc. */ - bool need_fullpath; + * If need_fullpath is true and retro_load_game() is called: + * - retro_game_info::path is guaranteed to have a valid path + * - retro_game_info::data and retro_game_info::size are invalid + * + * If need_fullpath is false and retro_load_game() is called: + * - retro_game_info::path may be NULL + * - retro_game_info::data and retro_game_info::size are guaranteed + * to be valid + * + * See also: + * - RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY + * - RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY + */ + bool need_fullpath; - /* If true, the frontend is not allowed to extract any archives before + /* If true, the frontend is not allowed to extract any archives before * loading the real content. - * Necessary for certain libretro implementations that load games + * Necessary for certain libretro implementations that load games * from zipped archives. */ - bool block_extract; + bool block_extract; }; struct retro_game_geometry @@ -1926,27 +2820,99 @@ struct retro_system_av_info struct retro_variable { /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. - * If NULL, obtains the complete environment string if more + * If NULL, obtains the complete environment string if more * complex parsing is necessary. - * The environment string is formatted as key-value pairs + * The environment string is formatted as key-value pairs * delimited by semicolons as so: * "key1=value1;key2=value2;..." */ const char *key; - + /* Value to be obtained. If key does not exist, it is set to NULL. */ const char *value; }; +struct retro_core_option_display +{ + /* Variable to configure in RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY */ + const char *key; + + /* Specifies whether variable should be displayed + * when presenting core options to the user */ + bool visible; +}; + +/* Maximum number of values permitted for a core option + * > 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 +{ + /* Expected option value */ + const char *value; + + /* Human-readable value label. If NULL, value itself + * will be displayed by the frontend */ + const char *label; +}; + +struct retro_core_option_definition +{ + /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. */ + const char *key; + + /* Human-readable core option description (used as menu label) */ + const char *desc; + + /* Human-readable core option information (used as menu sublabel) */ + const char *info; + + /* Array of retro_core_option_value structs, terminated by NULL */ + struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX]; + + /* Default core option value. Must match one of the values + * in the retro_core_option_value array, otherwise will be + * ignored */ + const char *default_value; +}; + +struct retro_core_options_intl +{ + /* Pointer to an array of retro_core_option_definition structs + * - US English implementation + * - Must point to a valid array */ + struct retro_core_option_definition *us; + + /* Pointer to an array of retro_core_option_definition structs + * - Implementation for current frontend language + * - May be NULL */ + struct retro_core_option_definition *local; +}; + struct retro_game_info { const char *path; /* Path to game, UTF-8 encoded. - * Usually used as a reference. - * May be NULL if rom was loaded from stdin - * or similar. - * retro_system_info::need_fullpath guaranteed + * Sometimes used as a reference for building other paths. + * May be NULL if game was loaded from stdin or similar, + * but in this case some cores will be unable to load `data`. + * So, it is preferable to fabricate something here instead + * of passing NULL, which will help more cores to succeed. + * retro_system_info::need_fullpath requires * that this path is valid. */ - const void *data; /* Memory buffer of loaded game. Will be NULL + const void *data; /* Memory buffer of loaded game. Will be NULL * if need_fullpath was set. */ size_t size; /* Size of memory buffer. */ const char *meta; /* String of implementation specific meta-data. */ @@ -1982,27 +2948,68 @@ 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 +/* Environment callback. Gives implementations a way of performing * uncommon tasks. Extensible. */ typedef bool (RETRO_CALLCONV *retro_environment_t)(unsigned cmd, void *data); -/* Render a frame. Pixel format is 15-bit 0RGB1555 native endian +/* Render a frame. Pixel format is 15-bit 0RGB1555 native endian * unless changed (see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT). * * Width and height specify dimensions of buffer. * Pitch specifices length in bytes between two lines in buffer. * - * For performance reasons, it is highly recommended to have a frame + * For performance reasons, it is highly recommended to have a frame * that is packed in memory, i.e. pitch == width * byte_per_pixel. - * Certain graphic APIs, such as OpenGL ES, do not like textures + * Certain graphic APIs, such as OpenGL ES, do not like textures * that are not packed in memory. */ typedef void (RETRO_CALLCONV *retro_video_refresh_t)(const void *data, unsigned width, unsigned height, size_t pitch); -/* Renders a single audio frame. Should only be used if implementation +/* Renders a single audio frame. Should only be used if implementation * generates a single sample at a time. * Format is signed 16-bit native endian. */ @@ -2020,20 +3027,20 @@ typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_t)(const int16_t *data, /* Polls input. */ typedef void (RETRO_CALLCONV *retro_input_poll_t)(void); -/* Queries for input for player 'port'. device will be masked with +/* Queries for input for player 'port'. device will be masked with * RETRO_DEVICE_MASK. * - * Specialization of devices such as RETRO_DEVICE_JOYPAD_MULTITAP that + * Specialization of devices such as RETRO_DEVICE_JOYPAD_MULTITAP that * have been set with retro_set_controller_port_device() * will still use the higher level RETRO_DEVICE_JOYPAD to request input. */ -typedef int16_t (RETRO_CALLCONV *retro_input_state_t)(unsigned port, unsigned device, +typedef int16_t (RETRO_CALLCONV *retro_input_state_t)(unsigned port, unsigned device, unsigned index, unsigned id); -/* Sets callbacks. retro_set_environment() is guaranteed to be called +/* Sets callbacks. retro_set_environment() is guaranteed to be called * before retro_init(). * - * The rest of the set_* functions are guaranteed to have been called + * The rest of the set_* functions are guaranteed to have been called * before the first call to retro_run() is made. */ RETRO_API void retro_set_environment(retro_environment_t); RETRO_API void retro_set_video_refresh(retro_video_refresh_t); @@ -2050,27 +3057,33 @@ RETRO_API void retro_deinit(void); * when the API is revised. */ RETRO_API unsigned retro_api_version(void); -/* Gets statically known system info. Pointers provided in *info +/* Gets statically known system info. Pointers provided in *info * must be statically allocated. * Can be called at any time, even before retro_init(). */ RETRO_API void retro_get_system_info(struct retro_system_info *info); /* Gets information about system audio/video timings and geometry. * Can be called only after retro_load_game() has successfully completed. - * NOTE: The implementation of this function might not initialize every + * NOTE: The implementation of this function might not initialize every * variable if needed. - * E.g. geom.aspect_ratio might not be initialized if core doesn't + * E.g. geom.aspect_ratio might not be initialized if core doesn't * desire a particular aspect ratio. */ RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info); /* Sets device to be used for player 'port'. - * By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all + * By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all * available ports. - * Setting a particular device type is not a guarantee that libretro cores - * will only poll input based on that particular device type. It is only a - * hint to the libretro core when a core cannot automatically detect the - * appropriate input device type on its own. It is also relevant when a - * core can change its behavior depending on device type. */ + * Setting a particular device type is not a guarantee that libretro cores + * will only poll input based on that particular device type. It is only a + * hint to the libretro core when a core cannot automatically detect the + * appropriate input device type on its own. It is also relevant when a + * core can change its behavior depending on device type. + * + * As part of the core's implementation of retro_set_controller_port_device, + * the core should call RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the + * frontend if the descriptions for any controls have changed as a + * result of changing the device type. + */ RETRO_API void retro_set_controller_port_device(unsigned port, unsigned device); /* Resets the current game. */ @@ -2078,18 +3091,18 @@ RETRO_API void retro_reset(void); /* Runs the game for one video frame. * During retro_run(), input_poll callback must be called at least once. - * + * * If a frame is not rendered for reasons where a game "dropped" a frame, - * this still counts as a frame, and retro_run() should explicitly dupe + * this still counts as a frame, and retro_run() should explicitly dupe * a frame if GET_CAN_DUPE returns true. * In this case, the video callback can take a NULL argument for data. */ RETRO_API void retro_run(void); -/* Returns the amount of data the implementation requires to serialize +/* Returns the amount of data the implementation requires to serialize * internal state (save states). - * Between calls to retro_load_game() and retro_unload_game(), the - * returned size is never allowed to be larger than a previous returned + * Between calls to retro_load_game() and retro_unload_game(), the + * returned size is never allowed to be larger than a previous returned * value, to ensure that the frontend can allocate a save state buffer once. */ RETRO_API size_t retro_serialize_size(void); @@ -2102,7 +3115,9 @@ RETRO_API bool retro_unserialize(const void *data, size_t size); RETRO_API void retro_cheat_reset(void); RETRO_API void retro_cheat_set(unsigned index, bool enabled, const char *code); -/* Loads a game. */ +/* Loads a game. + * Return true to indicate successful loading and false to indicate load failure. + */ RETRO_API bool retro_load_game(const struct retro_game_info *game); /* Loads a "special" kind of game. Should not be used, @@ -2112,7 +3127,7 @@ RETRO_API bool retro_load_game_special( const struct retro_game_info *info, size_t num_info ); -/* Unloads a currently loaded game. */ +/* Unloads the currently loaded game. Called before retro_deinit(void). */ RETRO_API void retro_unload_game(void); /* Gets region of game. */ diff --git a/libretro/libretro_core_options.inc b/libretro/libretro_core_options.inc new file mode 100644 index 0000000..1185763 --- /dev/null +++ b/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/libretro/retro_inline.h b/libretro/retro_inline.h new file mode 100644 index 0000000..14c038c --- /dev/null +++ b/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/version.mk b/version.mk new file mode 100644 index 0000000..8964270 --- /dev/null +++ b/version.mk @@ -0,0 +1 @@ +VERSION := 0.14.7 \ No newline at end of file diff --git a/wasm/Makefile b/wasm/Makefile index 3ba0580..acac7d3 100644 --- a/wasm/Makefile +++ b/wasm/Makefile @@ -26,8 +26,7 @@ endif CORE_DIR += .. -#VERSION := 0.13 -#export VERSION +CFLAGS += -DGB_VERSION=\"$(VERSION)\" CONF ?= debug BIN := $(CORE_DIR)/build/wasm_bin @@ -58,8 +57,10 @@ endif CFLAGS += -Werror -Wall -Wno-strict-aliasing -Wno-unknown-warning -Wno-unknown-warning-option -Wno-multichar -Wno-int-in-bool-context -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES # CFLAGS += -DGB_INTERNAL=1 # get access to internal APIs CFLAGS += -I$(CORE_DIR) -CFLAGS += -s WASM=1 -s USE_SDL=2 --preload-file $(BOOTROMS_DIR)@/BootROMs --preload-file $(CORE_DIR)/Shaders@/Shaders -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap', 'getValue', 'AsciiToString', 'FS']" +CFLAGS += -s WASM=1 -s USE_SDL=2 --preload-file $(BOOTROMS_DIR)@/BootROMs --preload-file $(CORE_DIR)/Shaders@/Shaders -s "EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap', 'getValue', 'AsciiToString', 'FS']" CFLAGS += -s USE_WEBGL2=1 +# -Werror implies -Wpass-failed=transform-warning and for some reason Clang fails to unroll some loops in apu.c +CFLAGS += -Wno-pass-failed # CFLAGS += -Wcast-align -Wover-aligned -s SAFE_HEAP=1 -s WARN_UNALIGNED=1 WASM_LDFLAGS := -lidbfs.js @@ -71,7 +72,8 @@ CFLAGS += -g -g4 CFLAGS += --cpuprofiler --memoryprofiler else ifeq ($(CONF), release) CFLAGS += -O3 -DNDEBUG --emit-symbol-map -CFLAGS += --llvm-lto 3 # might be unstable +CFLAGS += -flto +WASM_LDFLAGS += -flto else $(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release") endif