diff --git a/.github/actions/LICENSE b/.github/actions/LICENSE new file mode 100644 index 0000000..8c295a2 --- /dev/null +++ b/.github/actions/LICENSE @@ -0,0 +1,25 @@ +Blargg's Test ROMs by Shay Green + +Acid2 tests by Matt Currie under MIT: + +MIT License + +Copyright (c) 2020 Matt Currie + +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. \ No newline at end of file diff --git a/.github/actions/cgb-acid2.gbc b/.github/actions/cgb-acid2.gbc new file mode 100644 index 0000000..5f71bd3 Binary files /dev/null and b/.github/actions/cgb-acid2.gbc differ diff --git a/.github/actions/cgb_sound.gb b/.github/actions/cgb_sound.gb new file mode 100644 index 0000000..dc50471 Binary files /dev/null and b/.github/actions/cgb_sound.gb differ diff --git a/.github/actions/dmg-acid2.gb b/.github/actions/dmg-acid2.gb new file mode 100644 index 0000000..a25ef94 Binary files /dev/null and b/.github/actions/dmg-acid2.gb differ diff --git a/.github/actions/dmg_sound-2.gb b/.github/actions/dmg_sound-2.gb new file mode 100755 index 0000000..52dcf70 Binary files /dev/null and b/.github/actions/dmg_sound-2.gb differ diff --git a/.github/actions/install_deps.sh b/.github/actions/install_deps.sh new file mode 100755 index 0000000..1c9749e --- /dev/null +++ b/.github/actions/install_deps.sh @@ -0,0 +1,23 @@ +case `echo $1 | cut -d '-' -f 1` in + ubuntu) + sudo apt-get -qq update + sudo apt-get install -yq bison libpng-dev pkg-config libsdl2-dev + ( + cd `mktemp -d` + curl -L https://github.com/rednex/rgbds/archive/v0.4.0.zip > rgbds.zip + unzip rgbds.zip + cd rgbds-* + make -sj + sudo make install + cd .. + rm -rf * + ) + ;; + macos) + brew install rgbds sdl2 + ;; + *) + echo "Unsupported OS" + exit 1 + ;; +esac \ No newline at end of file diff --git a/.github/actions/oam_bug-2.gb b/.github/actions/oam_bug-2.gb new file mode 100755 index 0000000..a3f55af Binary files /dev/null and b/.github/actions/oam_bug-2.gb differ diff --git a/.github/actions/sanity_tests.sh b/.github/actions/sanity_tests.sh new file mode 100755 index 0000000..8984b26 --- /dev/null +++ b/.github/actions/sanity_tests.sh @@ -0,0 +1,33 @@ +set -e + +./build/bin/tester/sameboy_tester --jobs 5 \ + --length 40 .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 20 .github/actions/oam_bug-2.gb + +mv .github/actions/dmg{,-mode}-acid2.bmp + +./build/bin/tester/sameboy_tester \ +--dmg --length 10 .github/actions/dmg-acid2.gb + +set +e + +FAILED_TESTS=` +shasum .github/actions/*.bmp | grep -q -E -v \(\ +44ce0c7d49254df0637849c9155080ac7dc3ef3d\ \ .github/actions/cgb-acid2.bmp\|\ +dbcc438dcea13b5d1b80c5cd06bda2592cc5d9e0\ \ .github/actions/cgb_sound.bmp\|\ +0caadf9634e40247ae9c15ff71992e8f77bbf89e\ \ .github/actions/dmg-acid2.bmp\|\ +c50daed36c57a8170ff362042694786676350997\ \ .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 + exit 1 +fi + +echo Passed all tests \ No newline at end of file diff --git a/.github/workflows/sanity.yml b/.github/workflows/sanity.yml new file mode 100644 index 0000000..f460931 --- /dev/null +++ b/.github/workflows/sanity.yml @@ -0,0 +1,36 @@ +name: "Bulidability and Sanity" +on: push + +jobs: + sanity: + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, ubuntu-16.04] + cc: [gcc, clang] + include: + - os: macos-latest + cc: clang + extra_target: cocoa + exclude: + - os: macos-latest + cc: gcc + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Install deps + shell: bash + run: | + ./.github/actions/install_deps.sh ${{ matrix.os }} + - name: Build + run: | + ${{ matrix.cc }} -v; (make -j sdl tester libretro ${{ matrix.extra_target }} CONF=release CC=${{ matrix.cc }} || (echo "==== Build Failed ==="; make sdl tester libretro ${{ matrix.extra_target }} CONF=release CC=${{ matrix.cc }})) + - name: Sanity tests + shell: bash + run: | + ./.github/actions/sanity_tests.sh + - name: Upload binaries + uses: actions/upload-artifact@v1 + with: + name: sameboy-canary-${{ matrix.os }}-${{ matrix.cc }} + path: build/bin \ No newline at end of file diff --git a/BootROMs/SameBoyLogo.png b/BootROMs/SameBoyLogo.png new file mode 100644 index 0000000..c7cfc08 Binary files /dev/null and b/BootROMs/SameBoyLogo.png differ diff --git a/BootROMs/SameboyLogo.1bpp b/BootROMs/SameboyLogo.1bpp deleted file mode 100644 index b219f7d..0000000 Binary files a/BootROMs/SameboyLogo.1bpp and /dev/null differ diff --git a/BootROMs/SameboyLogo.png b/BootROMs/SameboyLogo.png deleted file mode 100644 index 4bc9706..0000000 Binary files a/BootROMs/SameboyLogo.png and /dev/null differ diff --git a/BootROMs/cgb_boot.asm b/BootROMs/cgb_boot.asm index ee0198a..dc3544f 100644 --- a/BootROMs/cgb_boot.asm +++ b/BootROMs/cgb_boot.asm @@ -1,31 +1,41 @@ -; Sameboy CGB bootstrap ROM +; SameBoy CGB bootstrap ROM ; Todo: use friendly names for HW registers instead of magic numbers SECTION "BootCode", ROM0[$0] Start: ; Init stack pointer ld sp, $fffe - xor a -; Clear chosen input palette - ldh [InputPalette], a -; Clear title checksum - ldh [TitleChecksum], a ; Clear memory VRAM - ld hl, $8000 - call ClearMemoryPage - ld h, $d0 + call ClearMemoryPage8000 + ld a, 2 + ld c, $70 + ld [c], a +; Clear RAM Bank 2 (Like the original boot ROM) + ld h, $D0 call ClearMemoryPage + ld [c], a ; Clear OAM - ld hl, $fe00 + ld h, $fe ld c, $a0 - xor a .clearOAMLoop ldi [hl], a dec c jr nz, .clearOAMLoop -; Init Audio +; Init waveform + ld c, $10 +.waveformLoop + ldi [hl], a + cpl + dec c + jr nz, .waveformLoop + +; Clear chosen input palette + ldh [InputPalette], a +; Clear title checksum + ldh [TitleChecksum], a + ld a, $80 ldh [$26], a ldh [$11], a @@ -34,8 +44,7 @@ Start: ldh [$25], a ld a, $77 ldh [$24], a - - call InitWaveform + ld hl, $FF30 ; Init BG palette ld a, $fc @@ -52,93 +61,82 @@ Start: .loadLogoLoop ld a, [de] ; Read 2 rows ld b, a - call DoubleBitsAndWriteRow - call DoubleBitsAndWriteRow + call DoubleBitsAndWriteRowTwice inc de ld a, e - xor $34 ; End of logo + cp $34 ; End of logo jr nz, .loadLogoLoop call ReadTrademarkSymbol ; Clear the second VRAM bank ld a, 1 ldh [$4F], a - xor a - ld hl, $8000 - call ClearMemoryPage + call ClearMemoryPage8000 + call LoadTileset -; Copy Sameboy Logo - ld de, SameboyLogo - ld hl, $8080 - ld c, (SameboyLogoEnd - SameboyLogo) / 2 -.sameboyLogoLoop - ld a, [de] - ldi [hl], a - inc hl - inc de - ld a, [de] - ldi [hl], a - inc hl - inc de - dec c - jr nz, .sameboyLogoLoop - -; Copy (unresized) ROM logo - ld de, $104 - ld c, 6 -.CGBROMLogoLoop - push bc - call ReadCGBLogoTile - pop bc - dec c - jr nz, .CGBROMLogoLoop - inc hl - call ReadTrademarkSymbol - -; Load Tilemap - ld hl, $98C2 ld b, 3 - ld a, 8 IF DEF(FAST) xor a ldh [$4F], a -ENDC +ELSE +; Load Tilemap + ld hl, $98C2 + ld d, 3 + ld a, 8 .tilemapLoop ld c, $10 .tilemapRowLoop - ld [hl], a + call .write_with_palette + + ; Repeat the 3 tiles common between E and B. This saves 27 bytes after + ; compression, with a cost of 17 bytes of code. push af -IF !DEF(FAST) - ; Switch to second VRAM Bank - ld a, 1 - ldh [$4F], a - ld a, 8 - ld [hl], a - ; Switch to back first VRAM Bank - xor a - ldh [$4F], a -ENDC + sub $20 + sub $3 + jr nc, .notspecial + add $20 + call .write_with_palette + dec c +.notspecial pop af - ldi [hl], a - inc a + + add d ; d = 3 for SameBoy logo, d = 1 for Nintendo logo dec c jr nz, .tilemapRowLoop + sub 44 + push de ld de, $10 add hl, de + pop de dec b jr nz, .tilemapLoop - cp $38 - jr nz, .doneTilemap + dec d + jr z, .endTilemap + dec d - ld hl, $99a7 - ld b, 1 - ld c, 7 + ld a, $38 + ld l, $a7 + ld bc, $0107 jr .tilemapRowLoop -.doneTilemap + +.write_with_palette + push af + ; Switch to second VRAM Bank + ld a, 1 + ldh [$4F], a + ld [hl], 8 + ; Switch to back first VRAM Bank + xor a + ldh [$4F], a + pop af + ldi [hl], a + ret +.endTilemap +ENDC ; Expand Palettes ld de, AnimationColors @@ -146,45 +144,44 @@ ENDC ld hl, BgPalettes xor a .expandPalettesLoop: -IF !DEF(FAST) cpl -ENDC ; One white - ldi [hl], a - ldi [hl], a + ld [hli], a + ld [hli], a -IF DEF(FAST) - ; 3 more whites - ldi [hl], a - ldi [hl], a - ldi [hl], a - ldi [hl], a - ldi [hl], a - ldi [hl], a -ELSE - ; The actual color + ; Mixed with white ld a, [de] - inc de - ldi [hl], a + inc e + or $20 + ld b, a + ld a, [de] - inc de - ldi [hl], a + dec e + or $84 + rra + rr b + ld [hl], b + inc l + ld [hli], a + + ; One black + xor a + ld [hli], a + ld [hli], a + + ; One color + ld a, [de] + inc e + ld [hli], a + ld a, [de] + inc e + ld [hli], a xor a - ; Two blacks - ldi [hl], a - ldi [hl], a - ldi [hl], a - ldi [hl], a -ENDC - dec c jr nz, .expandPalettesLoop - ld hl, BgPalettes - ld d, 64 ; Length of write - ld e, 0 ; Index of write - call LoadBGPalettes + call LoadPalettesFromHRAM ; Turn on LCD ld a, $91 @@ -193,8 +190,10 @@ ENDC IF !DEF(FAST) call DoIntroAnimation + ld a, 45 + ldh [WaitLoopCounter], a ; Wait ~0.75 seconds - ld b, 45 + ld b, a call WaitBFrames ; Play first sound @@ -206,10 +205,6 @@ IF !DEF(FAST) ld a, $c1 call PlaySound -; Wait ~0.5 seconds - ld a, 30 - ldh [WaitLoopCounter], a - .waitLoop call GetInputPaletteIndex call WaitFrame @@ -221,6 +216,9 @@ ELSE call PlaySound ENDC call Preboot +IF DEF(AGB) + ld b, 1 +ENDC ; Will be filled with NOPs @@ -229,7 +227,6 @@ BootGame: ldh [$50], a SECTION "MoreStuff", ROM0[$200] - ; Game Palettes Data TitleChecksums: db $00 ; Default @@ -491,7 +488,7 @@ ENDM palette_comb 4, 3, 28 palette_comb 28, 3, 6 palette_comb 4, 28, 29 - ; Sameboy "Exclusives" + ; SameBoy "Exclusives" palette_comb 30, 30, 30 ; CGA palette_comb 31, 31, 31 ; DMG LCD palette_comb 28, 4, 1 @@ -528,35 +525,35 @@ Palettes: dw $0000, $4200, $037F, $7FFF dw $7FFF, $7E8C, $7C00, $0000 dw $7FFF, $1BEF, $6180, $0000 - ; Sameboy "Exclusives" + ; SameBoy "Exclusives" dw $7FFF, $7FEA, $7D5F, $0000 ; CGA 1 dw $4778, $3290, $1D87, $0861 ; DMG LCD -KeyCombinationPalettes - db 1 ; Right - db 48 ; Left - db 5 ; Up - db 8 ; Down - db 0 ; Right + A - db 40 ; Left + A - db 43 ; Up + A - db 3 ; Down + A - db 6 ; Right + B - db 7 ; Left + B - db 28 ; Up + B - db 49 ; Down + B - ; Sameboy "Exclusives" - db 51 ; Right + A + B - db 52 ; Left + A + B - db 53 ; Up + A + B - db 54 ; Down + A + B +KeyCombinationPalettes: + db 1 * 3 ; Right + db 48 * 3 ; Left + db 5 * 3 ; Up + db 8 * 3 ; Down + db 0 * 3 ; Right + A + db 40 * 3 ; Left + A + db 43 * 3 ; Up + A + db 3 * 3 ; Down + A + db 6 * 3 ; Right + B + db 7 * 3 ; Left + B + db 28 * 3 ; Up + B + db 49 * 3 ; Down + B + ; SameBoy "Exclusives" + db 51 * 3 ; Right + A + B + db 52 * 3 ; Left + A + B + db 53 * 3 ; Up + A + B + db 54 * 3 ; Down + A + B TrademarkSymbol: db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c -SameboyLogo: - incbin "SameboyLogo.1bpp" -SameboyLogoEnd: +SameBoyLogo: + incbin "SameBoyLogo.pb12" + AnimationColors: dw $7FFF ; White @@ -569,11 +566,10 @@ AnimationColors: dw $7102 ; Blue AnimationColorsEnd: -DMGPalettes: - dw $7FFF, $32BF, $00D0, $0000 - ; Helper Functions -DoubleBitsAndWriteRow: +DoubleBitsAndWriteRowTwice: + call .twice +.twice ; Double the most significant 4 bits, b is shifted by 4 ld a, 4 ld c, 0 @@ -616,13 +612,18 @@ PlaySound: ldh [$14], a ret +ClearMemoryPage8000: + ld hl, $8000 ; Clear from HL to HL | 0x2000 ClearMemoryPage: + xor a ldi [hl], a bit 5, h jr z, ClearMemoryPage ret +ReadTwoTileLines: + call ReadTileLine ; c = $f0 for even lines, $f for odd lines. ReadTileLine: ld a, [de] @@ -642,34 +643,108 @@ ReadTileLine: .dontSwap inc hl ldi [hl], a + swap c ret ReadCGBLogoHalfTile: - ld c, $f0 - call ReadTileLine - ld c, $f - call ReadTileLine - inc e - ld c, $f0 - call ReadTileLine - ld c, $f - call ReadTileLine + call .do_twice +.do_twice + call ReadTwoTileLines inc e + ld a, e ret -ReadCGBLogoTile: +; LoadTileset using PB12 codec, 2020 Jakub Kądziołka +; (based on PB8 codec, 2019 Damian Yerrick) + +SameBoyLogo_dst = $8080 +SameBoyLogo_length = (128 * 24) / 64 + +LoadTileset: + ld hl, SameBoyLogo + ld de, SameBoyLogo_dst - 1 + ld c, SameBoyLogo_length +.refill + ; Register map for PB12 decompression + ; HL: source address in boot ROM + ; DE: destination address in VRAM + ; A: Current literal value + ; B: Repeat bits, terminated by 1000... + ; Source address in HL lets the repeat bits go straight to B, + ; bypassing A and avoiding spilling registers to the stack. + ld b, [hl] + dec b + jr z, .sameboyLogoEnd + inc b + inc hl + + ; Shift a 1 into lower bit of shift value. Once this bit + ; reaches the carry, B becomes 0 and the byte is over + scf + rl b + +.loop + ; If not a repeat, load a literal byte + jr c, .simple_repeat + sla b + jr c, .shifty_repeat + ld a, [hli] + jr .got_byte +.shifty_repeat + sla b + jr nz, .no_refill_during_shift + ld b, [hl] ; see above. Also, no, factoring it out into a callable + inc hl ; routine doesn't save bytes, even with conditional calls + scf + rl b +.no_refill_during_shift + ld c, a + jr nc, .shift_left + srl a + db $fe ; eat the add a with cp d8 +.shift_left + add a + sla b + jr c, .go_and + or c + db $fe ; eat the and c with cp d8 +.go_and + and c + jr .got_byte +.simple_repeat + sla b + jr c, .got_byte + ; far repeat + dec de + ld a, [de] + inc de +.got_byte + inc de + ld [de], a + sla b + jr nz, .loop + jr .refill + +; End PB12 decoding. The rest uses HL as the destination +.sameboyLogoEnd + ld h, d + ld l, $80 + +; Copy (unresized) ROM logo + ld de, $104 +.CGBROMLogoLoop + ld c, $f0 call ReadCGBLogoHalfTile - ld a, e add a, 22 ld e, a call ReadCGBLogoHalfTile - ld a, e sub a, 22 ld e, a - ret - - + cp $1c + jr nz, .CGBROMLogoLoop + inc hl + ; fallthrough ReadTrademarkSymbol: ld de, TrademarkSymbol ld c,$08 @@ -682,38 +757,23 @@ ReadTrademarkSymbol: jr nz, .loadTrademarkSymbolLoop ret -LoadObjPalettes: - ld c, $6A - jr LoadPalettes - -LoadBGPalettes: - ld c, $68 - -LoadPalettes: - ld a, $80 - or e - ld [c], a - inc c -.loop - ld a, [hli] - ld [c], a - dec d - jr nz, .loop - ret - - -AdvanceIntroAnimation: +DoIntroAnimation: + ; Animate the intro + ld a, 1 + ldh [$4F], a + ld d, 26 +.animationLoop + ld b, 2 + call WaitBFrames ld hl, $98C0 ld c, 3 ; Row count .loop ld a, [hl] cp $F ; Already blue jr z, .nextTile - inc a - ld [hl], a + inc [hl] and $7 - cp $1 ; Changed a white tile, go to next line - jr z, .nextLine + jr z, .nextLine ; Changed a white tile, go to next line .nextTile inc hl jr .loop @@ -723,48 +783,100 @@ AdvanceIntroAnimation: ld l, a inc hl dec c - ret z - jr .loop - -DoIntroAnimation: - ; Animate the intro - ld a, 1 - ldh [$4F], a - ld d, 26 -.animationLoop - ld b, 2 - call WaitBFrames - call AdvanceIntroAnimation + jr nz, .loop dec d jr nz, .animationLoop ret Preboot: IF !DEF(FAST) - call FadeOut + ld b, 32 ; 32 times to fade +.fadeLoop + ld c, 32 ; 32 colors to fade + ld hl, BgPalettes +.frameLoop + push bc + + ; Brighten Color + ld a, [hli] + ld e, a + ld a, [hld] + ld d, a + ; RGB(1,1,1) + ld bc, $421 + + ; Is blue maxed? + ld a, e + and $1F + cp $1F + jr nz, .blueNotMaxed + dec c +.blueNotMaxed + + ; Is green maxed? + ld a, e + cp $E0 + jr c, .greenNotMaxed + ld a, d + and $3 + cp $3 + jr nz, .greenNotMaxed + res 5, c +.greenNotMaxed + + ; Is red maxed? + ld a, d + and $7C + cp $7C + jr nz, .redNotMaxed + res 2, b +.redNotMaxed + + ; add de, bc + ; ld [hli], de + ld a, e + add c + ld [hli], a + ld a, d + adc b + ld [hli], a + pop bc + + dec c + jr nz, .frameLoop + + call WaitFrame + call LoadPalettesFromHRAM + call WaitFrame + dec b + jr nz, .fadeLoop ENDC + ld a, 1 call ClearVRAMViaHDMA - ; Select the first bank - xor a - ldh [$4F], a - cpl + call _ClearVRAMViaHDMA + call ClearVRAMViaHDMA ; A = $40, so it's bank 0 + ld a, $ff ldh [$00], a - call ClearVRAMViaHDMA - + ; Final values for CGB mode - ld de, $ff56 + ld d, a + ld e, c ld l, $0d - + ld a, [$143] bit 7, a call z, EmulateDMG + bit 7, a + ldh [$4C], a ldh a, [TitleChecksum] ld b, a - + + jr z, .skipDMGForCGBCheck ldh a, [InputPalette] and a jr nz, .emulateDMGForCGBGame +.skipDMGForCGBCheck IF DEF(AGB) ; Set registers to match the original AGB-CGB boot ; AF = $1100, C = 0 @@ -772,7 +884,7 @@ IF DEF(AGB) ld c, a add a, $11 ld h, c - ld b, 1 + ; B is set to 1 after ret ELSE ; Set registers to match the original CGB boot ; AF = $1180, C = 0 @@ -787,7 +899,15 @@ ENDC .emulateDMGForCGBGame call EmulateDMG ldh [$4C], a - ld a, $1; + ld a, $1 + ret + +GetKeyComboPalette: + ld hl, KeyCombinationPalettes - 1 ; Return value is 1-based, 0 means nothing down + ld c, a + ld b, 0 + add hl, bc + ld a, [hl] ret EmulateDMG: @@ -801,11 +921,7 @@ EmulateDMG: ldh a, [InputPalette] and a jr z, .nothingDown - ld hl, KeyCombinationPalettes - 1 ; Return value is 1-based, 0 means nothing down - ld c ,a - ld b, 0 - add hl, bc - ld a, [hl] + call GetKeyComboPalette jr .paletteFromKeys .nothingDown ld a, b @@ -814,8 +930,7 @@ EmulateDMG: call LoadPalettesFromIndex ld a, 4 ; Set the final values for DMG mode - ld d, 0 - ld e, $8 + ld de, 8 ld l, $7c ret @@ -824,7 +939,7 @@ GetPaletteIndex: ld a, [hl] ; Old Licensee cp $33 jr z, .newLicensee - cp 1 ; Nintendo + dec a ; 1 = Nintendo jr nz, .notNintendo jr .doChecksum .newLicensee @@ -839,22 +954,22 @@ GetPaletteIndex: .doChecksum ld l, $34 ld c, $10 - ld b, 0 + xor a .checksumLoop - ld a, [hli] - add b - ld b, a + add [hl] + inc l dec c jr nz, .checksumLoop + ld b, a ; c = 0 ld hl, TitleChecksums .searchLoop ld a, l - cp ChecksumsEnd & $FF - jr z, .notNintendo + sub LOW(ChecksumsEnd) ; use sub to zero out a + ret z ld a, [hli] cp b jr nz, .searchLoop @@ -888,16 +1003,16 @@ GetPaletteIndex: xor a ret -LoadPalettesFromIndex: ; a = index of combination - ld b, a - ; Multiply by 3 - add b - add b - +; optimizations in callers rely on this returning with b = 0 +GetPaletteCombo: ld hl, PaletteCombinations ld b, 0 ld c, a add hl, bc + ret + +LoadPalettesFromIndex: ; a = index of combination + call GetPaletteCombo ; Obj Palettes ld e, 0 @@ -905,11 +1020,12 @@ LoadPalettesFromIndex: ; a = index of combination ld a, [hli] push hl ld hl, Palettes - ld b, 0 + ; b is already 0 ld c, a add hl, bc ld d, 8 - call LoadObjPalettes + ld c, $6A + call LoadPalettes pop hl bit 3, e jr nz, .loadBGPalette @@ -917,109 +1033,48 @@ LoadPalettesFromIndex: ; a = index of combination jr .loadObjPalette .loadBGPalette ;BG Palette - ld a, [hli] + ld c, [hl] + ; b is already 0 ld hl, Palettes - ld b, 0 - ld c, a add hl, bc ld d, 8 + jr LoadBGPalettes + +LoadPalettesFromHRAM: + ld hl, BgPalettes + ld d, 64 + +LoadBGPalettes: ld e, 0 - call LoadBGPalettes - ret + ld c, $68 -BrightenColor: +LoadPalettes: + ld a, $80 + or e + ld [c], a + inc c +.loop ld a, [hli] - ld e, a - ld a, [hld] - ld d, a - ; RGB(1,1,1) - ld bc, $421 - - ; Is blue maxed? - ld a, e - and $1F - cp $1F - jr nz, .blueNotMaxed - res 0, c -.blueNotMaxed - - ; Is green maxed? - ld a, e - and $E0 - cp $E0 - jr nz, .greenNotMaxed - ld a, d - and $3 - cp $3 - jr nz, .greenNotMaxed - res 5, c -.greenNotMaxed - - ; Is red maxed? - ld a, d - and $7C - cp $7C - jr nz, .redNotMaxed - res 2, b -.redNotMaxed - - ; Add de to bc - push hl - ld h, d - ld l, e - add hl, bc - ld d, h - ld e, l - pop hl - - ld a, e - ld [hli], a - ld a, d - ld [hli], a + ld [c], a + dec d + jr nz, .loop ret -FadeOut: - ld b, 32 ; 32 times to fade -.fadeLoop - ld c, 32 ; 32 colors to fade - ld hl, BgPalettes -.frameLoop - push bc - call BrightenColor - pop bc - dec c - jr nz, .frameLoop - - call WaitFrame - call WaitFrame - ld hl, BgPalettes - ld d, 64 ; Length of write - ld e, 0 ; Index of write - call LoadBGPalettes - dec b - ret z - jr .fadeLoop - ClearVRAMViaHDMA: - ld hl, $FF51 - - ; Src - ld a, $D0 - ld [hli], a - xor a - ld [hli], a - - ; Dest - ld a, $98 - ld [hli], a - ld a, $A0 - ld [hli], a - - ; Do it - ld a, $12 - ld [hli], a + ldh [$4F], a + ld hl, HDMAData +_ClearVRAMViaHDMA: + ld c, $51 + ld b, 5 +.loop + ld a, [hli] + ldh [c], a + inc c + dec b + jr nz, .loop ret +; clobbers AF and HL GetInputPaletteIndex: ld a, $20 ; Select directions ldh [$00], a @@ -1027,11 +1082,10 @@ GetInputPaletteIndex: cpl and $F ret z ; No direction keys pressed, no palette - push bc - ld c, 0 + ld l, 0 .directionLoop - inc c + inc l rra jr nc, .directionLoop @@ -1044,40 +1098,24 @@ GetInputPaletteIndex: rla rla and $C - add c - ld b, a + add l + ld l, a ldh a, [InputPalette] - ld c, a - ld a, b - ldh [InputPalette], a - cp c - pop bc + cp l ret z ; No change, don't load + ld a, l + ldh [InputPalette], a ; Slide into change Animation Palette ChangeAnimationPalette: - push af - push hl push bc push de - ld hl, KeyCombinationPalettes - 1 ; Input palettes are 1-based, 0 means nothing down - ld c ,a - ld b, 0 - add hl, bc - ld a, [hl] - ld b, a - ; Multiply by 3 - add b - add b - - ld hl, PaletteCombinations + 2; Background Palette - ld b, 0 - ld c, a - add hl, bc - ld a, [hl] + call GetKeyComboPalette + call GetPaletteCombo + inc l + inc l + ld c, [hl] ld hl, Palettes + 1 - ld b, 0 - ld c, a add hl, bc ld a, [hld] cp $7F ; Is white color? @@ -1087,60 +1125,83 @@ ChangeAnimationPalette: .isWhite push af ld a, [hli] + push hl - ld hl, BgPalettes ; First color, all palette + ld hl, BgPalettes ; First color, all palettes + call ReplaceColorInAllPalettes + ld l, LOW(BgPalettes + 2) ; Second color, all palettes call ReplaceColorInAllPalettes pop hl - ldh [BgPalettes + 2], a ; Second color, first palette + ldh [BgPalettes + 6], a ; Fourth color, first palette ld a, [hli] push hl - ld hl, BgPalettes + 1 ; First color, all palette + ld hl, BgPalettes + 1 ; First color, all palettes + call ReplaceColorInAllPalettes + ld l, LOW(BgPalettes + 3) ; Second color, all palettes call ReplaceColorInAllPalettes pop hl - ldh [BgPalettes + 3], a ; Second color, first palette + ldh [BgPalettes + 7], a ; Fourth color, first palette + pop af jr z, .isNotWhite inc hl inc hl .isNotWhite + ; Mixing code by ISSOtm + ldh a, [BgPalettes + 7 * 8 + 2] + and ~$21 + ld b, a ld a, [hli] - ldh [BgPalettes + 7 * 8 + 2], a ; Second color, 7th palette + and ~$21 + add a, b + ld b, a + ld a, [BgPalettes + 7 * 8 + 3] + res 2, a ; and ~$04, but not touching carry + ld c, [hl] + res 2, c ; and ~$04, but not touching carry + adc a, c + rra ; Carry sort of "extends" the accumulator, we're bringing that bit back home + ld [BgPalettes + 7 * 8 + 3], a + ld a, b + rra + ld [BgPalettes + 7 * 8 + 2], a + dec l + ld a, [hli] - ldh [BgPalettes + 7 * 8 + 3], a ; Second color, 7th palette + ldh [BgPalettes + 7 * 8 + 6], a ; Fourth color, 7th palette + ld a, [hli] + ldh [BgPalettes + 7 * 8 + 7], a ; Fourth color, 7th palette + ld a, [hli] ldh [BgPalettes + 4], a ; Third color, first palette - ld a, [hl] + ld a, [hli] ldh [BgPalettes + 5], a ; Third color, first palette + call WaitFrame - ld hl, BgPalettes - ld d, 64 ; Length of write - ld e, 0 ; Index of write - call LoadBGPalettes + call LoadPalettesFromHRAM ; Delay the wait loop while the user is selecting a palette - ld a, 30 + ld a, 45 ldh [WaitLoopCounter], a pop de pop bc - pop hl - pop af ret ReplaceColorInAllPalettes: ld de, 8 - ld c, 8 + ld c, e .loop ld [hl], a add hl, de dec c jr nz, .loop ret - + LoadDMGTilemap: push af call WaitFrame - ld a,$19 ; Trademark symbol + ld a, $19 ; Trademark symbol ld [$9910], a ; ... put in the superscript position ld hl,$992f ; Bottom right corner of the logo ld c,$c ; Tiles in a logo row @@ -1150,27 +1211,20 @@ LoadDMGTilemap: ldd [hl], a dec c jr nz, .tilemapLoop - ld l,$0f ; Jump to top row + ld l, $0f ; Jump to top row jr .tilemapLoop .tilemapDone pop af ret -InitWaveform: - ld hl, $FF30 -; Init waveform - xor a - ld c, $10 -.waveformLoop - ldi [hl], a - cpl - dec c - jr nz, .waveformLoop - ret +HDMAData: + db $88, $00, $98, $A0, $12 + db $88, $00, $80, $00, $40 -SECTION "ROMMax", ROM0[$900] - ; Prevent us from overflowing - ds 1 +BootEnd: +IF BootEnd > $900 + FAIL "BootROM overflowed: {BootEnd}" +ENDC SECTION "HRAM", HRAM[$FF80] TitleChecksum: @@ -1180,4 +1234,4 @@ BgPalettes: InputPalette: ds 1 WaitLoopCounter: - ds 1 \ No newline at end of file + ds 1 diff --git a/BootROMs/dmg_boot.asm b/BootROMs/dmg_boot.asm index 46b389a..97a12e7 100644 --- a/BootROMs/dmg_boot.asm +++ b/BootROMs/dmg_boot.asm @@ -1,4 +1,4 @@ -; Sameboy CGB bootstrap ROM +; SameBoy DMG bootstrap ROM ; Todo: use friendly names for HW registers instead of magic numbers SECTION "BootCode", ROM0[$0] Start: @@ -24,7 +24,7 @@ Start: ldh [$24], a ; Init BG palette - ld a, $fc + ld a, $54 ldh [$47], a ; Load logo from ROM. @@ -69,14 +69,36 @@ Start: jr .tilemapLoop .tilemapDone + ld a, 30 + ldh [$ff42], a + ; Turn on LCD ld a, $91 ldh [$40], a -; Wait ~0.75 seconds - ld b, 45 - call WaitBFrames - + ld d, (-119) & $FF + ld c, 15 + +.animate + call WaitFrame + ld a, d + sra a + sra a + ldh [$ff42], a + ld a, d + add c + ld d, a + ld a, c + cp 8 + jr nz, .noPaletteChange + ld a, $A8 + ldh [$47], a +.noPaletteChange + dec c + jr nz, .animate + ld a, $fc + ldh [$47], a + ; Play first sound ld a, $83 call PlaySound @@ -85,9 +107,11 @@ Start: ; Play second sound ld a, $c1 call PlaySound + -; Wait ~1.15 seconds - ld b, 70 + +; Wait ~1 second + ld b, 60 call WaitBFrames ; Set registers to match the original DMG boot diff --git a/BootROMs/pb12.c b/BootROMs/pb12.c new file mode 100644 index 0000000..3f6d5f8 --- /dev/null +++ b/BootROMs/pb12.c @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include + +void opts(uint8_t byte, uint8_t *options) +{ + *(options++) = byte | ((byte << 1) & 0xff); + *(options++) = byte & (byte << 1); + *(options++) = byte | ((byte >> 1) & 0xff); + *(options++) = byte & (byte >> 1); +} + +int main() +{ + static uint8_t source[0x4000]; + size_t size = read(STDIN_FILENO, &source, sizeof(source)); + unsigned pos = 0; + assert(size <= 0x4000); + while (size && source[size - 1] == 0) { + size--; + } + + uint8_t literals[8]; + size_t literals_size = 0; + unsigned bits = 0; + unsigned control = 0; + unsigned prev[2] = {-1, -1}; // Unsigned to allow "not set" values + + while (true) { + + uint8_t byte = 0; + if (pos == size){ + if (bits == 0) break; + } + else { + byte = source[pos++]; + } + + if (byte == prev[0] || byte == prev[1]) { + bits += 2; + control <<= 1; + control |= 1; + control <<= 1; + if (byte == prev[1]) { + control |= 1; + } + } + else { + bits += 2; + control <<= 2; + uint8_t options[4]; + opts(prev[1], options); + bool found = false; + for (unsigned i = 0; i < 4; i++) { + if (options[i] == byte) { + // 01 = modify + control |= 1; + + bits += 2; + control <<= 2; + control |= i; + found = true; + break; + } + } + if (!found) { + literals[literals_size++] = byte; + } + } + + prev[0] = prev[1]; + prev[1] = byte; + if (bits >= 8) { + uint8_t outctl = control >> (bits - 8); + assert(outctl != 1); + write(STDOUT_FILENO, &outctl, 1); + write(STDOUT_FILENO, literals, literals_size); + bits -= 8; + control &= (1 << bits) - 1; + literals_size = 0; + } + } + uint8_t end_byte = 1; + write(STDOUT_FILENO, &end_byte, 1); + + return 0; +} diff --git a/BootROMs/sgb_boot.asm b/BootROMs/sgb_boot.asm index 0574d29..cdb9d77 100644 --- a/BootROMs/sgb_boot.asm +++ b/BootROMs/sgb_boot.asm @@ -1,4 +1,4 @@ -; Sameboy CGB bootstrap ROM +; SameBoy SGB bootstrap ROM ; Todo: use friendly names for HW registers instead of magic numbers SECTION "BootCode", ROM0[$0] Start: @@ -82,9 +82,9 @@ Start: .sendCommand xor a - ldh [c], a + ld [c], a ld a, $30 - ldh [c], a + ld [c], a ldh a, [$80] call SendByte @@ -112,9 +112,9 @@ Start: ; Done bit ld a, $20 - ldh [c], a + ld [c], a ld a, $30 - ldh [c], a + ld [c], a ; Update command ldh a, [$80] @@ -128,10 +128,10 @@ Start: ; Write to sound registers for DMG compatibility ld c, $13 ld a, $c1 - ldh [c], a + ld [c], a inc c ld a, 7 - ldh [c], a + ld [c], a ; Init BG palette ld a, $fc @@ -168,9 +168,9 @@ SendByte: jr c, .zeroBit add a ; 10 -> 20 .zeroBit - ldh [c], a + ld [c], a ld a, $30 - ldh [c], a + ld [c], a dec d ret z jr .loop diff --git a/Cocoa/AppDelegate.h b/Cocoa/AppDelegate.h index 608a50c..22e0c36 100644 --- a/Cocoa/AppDelegate.h +++ b/Cocoa/AppDelegate.h @@ -1,6 +1,6 @@ #import -@interface AppDelegate : NSObject +@interface AppDelegate : NSObject @property IBOutlet NSWindow *preferencesWindow; @property (strong) IBOutlet NSView *graphicsTab; diff --git a/Cocoa/AppDelegate.m b/Cocoa/AppDelegate.m index bbaa3ae..e54012f 100644 --- a/Cocoa/AppDelegate.m +++ b/Cocoa/AppDelegate.m @@ -1,7 +1,9 @@ #import "AppDelegate.h" #include "GBButtons.h" +#include "GBView.h" #include #import +#import @implementation AppDelegate { @@ -36,11 +38,20 @@ @"GBColorCorrection": @(GB_COLOR_CORRECTION_EMULATE_HARDWARE), @"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET), @"GBRewindLength": @(10), + @"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : GB_FRAME_BLENDING_MODE_ACCURATE), @"GBDMGModel": @(GB_MODEL_DMG_B), @"GBCGBModel": @(GB_MODEL_CGB_E), @"GBSGBModel": @(GB_MODEL_SGB2), + @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY), }]; + + [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{ + JOYAxes2DEmulateButtonsKey: @YES, + JOYHatsEmulateButtonsKey: @YES, + }]; + + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; } - (IBAction)toggleDeveloperMode:(id)sender @@ -92,4 +103,8 @@ return YES; } +- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification +{ + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:YES]; +} @end diff --git a/Cocoa/Document.h b/Cocoa/Document.h index 412e6ff..660d7bc 100644 --- a/Cocoa/Document.h +++ b/Cocoa/Document.h @@ -1,8 +1,11 @@ #import #include "GBView.h" #include "GBImageView.h" +#include "GBSplitView.h" -@interface Document : NSDocument +@class GBCheatWindowController; + +@interface Document : NSDocument @property (strong) IBOutlet GBView *view; @property (strong) IBOutlet NSTextView *consoleOutput; @property (strong) IBOutlet NSPanel *consoleWindow; @@ -27,9 +30,12 @@ @property (strong) IBOutlet NSTableView *spritesTableView; @property (strong) IBOutlet NSPanel *printerFeedWindow; @property (strong) IBOutlet NSImageView *feedImageView; -@property (strong) IBOutlet NSButton *feedSaveButton; @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; -(uint8_t) readMemory:(uint16_t) addr; -(void) writeMemory:(uint16_t) addr value:(uint8_t)value; diff --git a/Cocoa/Document.m b/Cocoa/Document.m index 3d3ad7b..ece8092 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -7,6 +7,8 @@ #include "HexFiend/HexFiend.h" #include "GBMemoryByteArray.h" #include "GBWarningPopover.h" +#include "GBCheatWindowController.h" +#include "GBTerminalTextFieldCell.h" /* 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!!! */ @@ -55,6 +57,14 @@ enum model { bool rewind; bool modelsChanging; + + NSCondition *audioLock; + GB_sample_t *audioBuffer; + size_t audioBufferSize; + size_t audioBufferPosition; + size_t audioBufferNeeded; + + bool borderModeChanged; } @property GBAudioClient *audioClient; @@ -67,8 +77,17 @@ enum model { - (void) printImage:(uint32_t *)image height:(unsigned) height topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin exposure:(unsigned) exposure; +- (void) gotNewSample:(GB_sample_t *)sample; +- (void) rumbleChanged:(double)amp; +- (void) loadBootROM:(GB_boot_rom_t)type; @end +static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self loadBootROM: type]; +} + static void vblank(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); @@ -118,6 +137,18 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; } +static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self gotNewSample:sample]; +} + +static void rumbleCallback(GB_gameboy_t *gb, double amp) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self rumbleChanged:amp]; +} + @implementation Document { GB_gameboy_t gb; @@ -127,12 +158,14 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, NSMutableArray *debugger_input_queue; } -- (instancetype)init { +- (instancetype)init +{ self = [super init]; if (self) { has_debugger_input = [[NSConditionLock alloc] initWithCondition:0]; debugger_input_queue = [[NSMutableArray alloc] init]; console_output_lock = [[NSRecursiveLock alloc] init]; + audioLock = [[NSCondition alloc] init]; } return self; } @@ -170,25 +203,82 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } } +- (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; + } +} + +- (void) updateBorderMode +{ + borderModeChanged = true; +} + +- (void) updateRumbleMode +{ + GB_set_rumble_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRumbleMode"]); +} + - (void) initCommon { GB_init(&gb, [self internalModel]); GB_set_user_data(&gb, (__bridge void *)(self)); + GB_set_boot_rom_load_callback(&gb, (GB_boot_rom_load_callback_t)boot_rom_load); GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); GB_set_log_callback(&gb, (GB_log_callback_t) consoleLog); GB_set_input_callback(&gb, (GB_input_callback_t) consoleInput); 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_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); + [self updatePalette]; GB_set_rgb_encode_callback(&gb, rgbEncode); GB_set_camera_get_pixel_callback(&gb, cameraGetPixel); 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_apu_set_sample_callback(&gb, audioCallback); + GB_set_rumble_callback(&gb, rumbleCallback); + [self updateRumbleMode]; +} + +- (void) updateMinSize +{ + self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&gb), GB_get_screen_height(&gb)); + if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&gb) || + self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&gb)) { + [self.mainWindow zoom:nil]; + } } - (void) vblank { [self.view flip]; + if (borderModeChanged) { + dispatch_sync(dispatch_get_main_queue(), ^{ + size_t previous_width = GB_get_screen_width(&gb); + GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); + if (GB_get_screen_width(&gb) != previous_width) { + [self.view screenSizeChanged]; + [self updateMinSize]; + } + }); + borderModeChanged = false; + } GB_set_pixels_output(&gb, self.view.pixels); if (self.vramWindow.isVisible) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -201,19 +291,93 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } } +- (void)gotNewSample:(GB_sample_t *)sample +{ + [audioLock lock]; + if (self.audioClient.isPlaying) { + if (audioBufferPosition == audioBufferSize) { + if (audioBufferSize >= 0x4000) { + audioBufferPosition = 0; + [audioLock unlock]; + return; + } + + if (audioBufferSize == 0) { + audioBufferSize = 512; + } + else { + audioBufferSize += audioBufferSize >> 2; + } + audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize); + } + audioBuffer[audioBufferPosition++] = *sample; + } + if (audioBufferPosition == audioBufferNeeded) { + [audioLock signal]; + audioBufferNeeded = 0; + } + [audioLock unlock]; +} + +- (void)rumbleChanged:(double)amp +{ + [_view setRumble:amp]; +} + - (void) run { 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) { - GB_apu_copy_buffer(&gb, buffer, nFrames); + [audioLock lock]; + + if (audioBufferPosition < nFrames) { + audioBufferNeeded = nFrames; + [audioLock wait]; + } + + if (stopping) { + memset(buffer, 0, nFrames * sizeof(*buffer)); + [audioLock unlock]; + return; + } + + if (audioBufferPosition >= nFrames && audioBufferPosition < nFrames + 4800) { + memcpy(buffer, audioBuffer, nFrames * sizeof(*buffer)); + memmove(audioBuffer, audioBuffer + nFrames, (audioBufferPosition - nFrames) * sizeof(*buffer)); + audioBufferPosition = audioBufferPosition - nFrames; + } + else { + memcpy(buffer, audioBuffer + (audioBufferPosition - nFrames), nFrames * sizeof(*buffer)); + audioBufferPosition = 0; + } + [audioLock unlock]; } andSampleRate:96000]; if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { [self.audioClient start]; } NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; + + /* Clear pending alarms, don't play alarms while playing */ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; + for (NSUserNotification *notification in [center scheduledNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeScheduledNotification:notification]; + break; + } + } + + for (NSUserNotification *notification in [center deliveredNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeDeliveredNotification:notification]; + break; + } + } + } + while (running) { if (rewind) { rewind = false; @@ -227,10 +391,34 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } } [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]); + unsigned time_to_alarm = GB_time_to_alarm(&gb); + + if (time_to_alarm) { + NSUserNotification *notification = [[NSUserNotification alloc] init]; + NSString *friendlyName = [[self.fileName 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.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm]; + notification.soundName = NSUserNotificationDefaultSoundName; + [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"]; + } + [_view setRumble:0]; stopping = false; } @@ -248,21 +436,32 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, if (GB_debugger_is_stopped(&gb)) { [self interruptDebugInputRead]; } + [audioLock lock]; stopping = true; + [audioLock signal]; + [audioLock unlock]; running = false; - while (stopping); + while (stopping) { + [audioLock lock]; + [audioLock signal]; + [audioLock unlock]; + } GB_debugger_set_disabled(&gb, false); } -- (void) loadBootROM +- (void) loadBootROM: (GB_boot_rom_t)type { - static NSString * const boot_names[] = {@"dmg_boot", @"cgb_boot", @"agb_boot", @"sgb_boot"}; - if ([self internalModel] == GB_MODEL_SGB2) { - GB_load_boot_rom(&gb, [[self bootROMPathForName:@"sgb2_boot"] UTF8String]); - } - else { - GB_load_boot_rom(&gb, [[self bootROMPathForName:boot_names[current_model - 1]] UTF8String]); - } + static NSString *const names[] = { + [GB_BOOT_ROM_DMG0] = @"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] = @"cgb_boot", + [GB_BOOT_ROM_AGB] = @"agb_boot", + }; + GB_load_boot_rom(&gb, [[self bootROMPathForName:names[type]] UTF8String]); } - (IBAction)reset:(id)sender @@ -274,8 +473,6 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, current_model = (enum model)[sender tag]; } - [self loadBootROM]; - if (!modelsChanging && [sender tag] == MODEL_NONE) { GB_reset(&gb); } @@ -287,11 +484,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [self.view screenSizeChanged]; } - self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&gb), GB_get_screen_height(&gb)); - if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&gb) || - self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&gb)) { - [self.mainWindow zoom:nil]; - } + [self updateMinSize]; if ([sender tag] != 0) { /* User explictly selected a model, save the preference */ @@ -325,15 +518,21 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, - (void)dealloc { [cameraSession stopRunning]; + self.view.gb = NULL; GB_free(&gb); if (cameraImage) { CVBufferRelease(cameraImage); } + if (audioBuffer) { + free(audioBuffer); + } } -- (void)windowControllerDidLoadNib:(NSWindowController *)aController { +- (void)windowControllerDidLoadNib:(NSWindowController *)aController +{ [super windowControllerDidLoadNib:aController]; - + // Interface Builder bug? + [self.consoleWindow setContentSize:self.consoleWindow.minSize]; /* Close Open Panels, if any */ for (NSWindow *window in [[NSApplication sharedApplication] windows]) { if ([window isKindOfClass:[NSOpenPanel class]]) { @@ -348,6 +547,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, self.debuggerSideViewInput.textColor = [NSColor whiteColor]; self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style; [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"]; + ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &gb; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateSideView) name:NSTextDidChangeNotification @@ -355,7 +555,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, self.consoleOutput.textContainerInset = NSMakeSize(4, 4); [self.view becomeFirstResponder]; - self.view.shouldBlendFrameWithPrevious = ![[NSUserDefaults standardUserDefaults] boolForKey:@"DisableFrameBlending"]; + self.view.frameBlendingMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; CGRect window_frame = self.mainWindow.frame; window_frame.size.width = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowWidth"], window_frame.size.width); @@ -365,15 +565,10 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised; - [self.feedSaveButton removeFromSuperview]; self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[self.fileURL path] lastPathComponent]]; - - /* contentView.superview.subviews.lastObject is the titlebar view */ - NSView *titleView = self.printerFeedWindow.contentView.superview.subviews.lastObject; - [titleView addSubview: self.feedSaveButton]; - self.feedSaveButton.frame = (NSRect){{268, 2}, {48, 17}}; - + self.debuggerSplitView.dividerColor = [NSColor clearColor]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateHighpassFilter) name:@"GBHighpassFilterChanged" @@ -384,6 +579,26 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, name:@"GBColorCorrectionChanged" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateFrameBlendingMode) + name:@"GBFrameBlendingModeChanged" + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updatePalette) + name:@"GBColorPaletteChanged" + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateBorderMode) + name:@"GBBorderModeChanged" + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateRumbleMode) + name:@"GBRumbleModeChanged" + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateRewindLength) name:@"GBRewindLengthChanged" @@ -426,7 +641,6 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, { hex_controller = [[HFController alloc] init]; [hex_controller setBytesPerColumn:1]; - [hex_controller setFont:[NSFont userFixedPitchFontOfSize:12]]; [hex_controller setEditMode:HFOverwriteMode]; [hex_controller setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]]; @@ -469,11 +683,13 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, self.memoryBankItem.enabled = false; } -+ (BOOL)autosavesInPlace { ++ (BOOL)autosavesInPlace +{ return YES; } -- (NSString *)windowNibName { +- (NSString *)windowNibName +{ // Override returning the nib file name of the document // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead. return @"Document"; @@ -487,9 +703,18 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, - (void) loadROM { NSString *rom_warnings = [self captureOutputForBlock:^{ - GB_load_rom(&gb, [self.fileName UTF8String]); - GB_load_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); 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]); + + } + else { + GB_load_rom(&gb, [self.fileName UTF8String]); + } + GB_load_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); + GB_load_cheats(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"cht"] 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]); }]; @@ -530,15 +755,9 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [[NSUserDefaults standardUserDefaults] setBool:!self.audioClient.isPlaying forKey:@"Mute"]; } -- (IBAction)toggleBlend:(id)sender -{ - self.view.shouldBlendFrameWithPrevious ^= YES; - [[NSUserDefaults standardUserDefaults] setBool:!self.view.shouldBlendFrameWithPrevious forKey:@"DisableFrameBlending"]; -} - - (BOOL)validateUserInterfaceItem:(id)anItem { - if([anItem action] == @selector(mute:)) { + if ([anItem action] == @selector(mute:)) { [(NSMenuItem*)anItem setState:!self.audioClient.isPlaying]; } else if ([anItem action] == @selector(togglePause:)) { @@ -548,9 +767,6 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) { [(NSMenuItem*)anItem setState:anItem.tag == current_model]; } - else if ([anItem action] == @selector(toggleBlend:)) { - [(NSMenuItem*)anItem setState:self.view.shouldBlendFrameWithPrevious]; - } else if ([anItem action] == @selector(interrupt:)) { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { return false; @@ -562,6 +778,9 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, else if ([anItem action] == @selector(connectPrinter:)) { [(NSMenuItem*)anItem setState:accessory == GBAccessoryPrinter]; } + else if ([anItem action] == @selector(toggleCheats:)) { + [(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)]; + } return [super validateUserInterfaceItem:anItem]; } @@ -588,8 +807,8 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, NSRect rect = window.contentView.frame; - int titlebarSize = window.contentView.superview.frame.size.height - rect.size.height; - int step = width / [[window screen] backingScaleFactor]; + unsigned titlebarSize = window.contentView.superview.frame.size.height - rect.size.height; + unsigned step = width / [[window screen] backingScaleFactor]; rect.size.width = floor(rect.size.width / step) * step + step; rect.size.height = rect.size.width * height / width + titlebarSize; @@ -670,9 +889,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } if (![console_output_timer isValid]) { - console_output_timer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 repeats:NO block:^(NSTimer * _Nonnull timer) { - [self appendPendingOutput]; - }]; + console_output_timer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:NO]; [[NSRunLoop mainRunLoop] addTimer:console_output_timer forMode:NSDefaultRunLoopMode]; } @@ -687,7 +904,8 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [self.consoleWindow orderBack:nil]; } -- (IBAction)consoleInput:(NSTextField *)sender { +- (IBAction)consoleInput:(NSTextField *)sender +{ NSString *line = [sender stringValue]; if ([line isEqualToString:@""] && lastConsoleInput) { line = lastConsoleInput; @@ -785,6 +1003,9 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [debugger_input_queue removeObjectAtIndex:0]; } [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; + if ((id)input == [NSNull null]) { + return NULL; + } return input? strdup([input UTF8String]): NULL; } @@ -865,7 +1086,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, { CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef) data); CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; + CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; CGImageRef iref = CGImageCreate(width, @@ -936,6 +1157,9 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, (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), + 160, 144); self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0]; self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest; } @@ -1100,6 +1324,23 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { if (!cameraSession) { + if (@available(macOS 10.14, *)) { + switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { + case AVAuthorizationStatusAuthorized: + break; + case AVAuthorizationStatusNotDetermined: { + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { + [self cameraRequestUpdate]; + }]; + return; + } + case AVAuthorizationStatusDenied: + case AVAuthorizationStatusRestricted: + GB_camera_updated(&gb); + return; + } + } + NSError *error; AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error]; @@ -1192,6 +1433,11 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } } +- (IBAction)toggleScrollingDisplay:(NSButton *)sender +{ + self.tilemapImageView.displayScrollRect = sender.state; +} + - (IBAction)vramTabChanged:(NSSegmentedControl *)sender { [self.vramTabView selectTabViewItemAtIndex:[sender selectedSegment]]; @@ -1300,7 +1546,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, NSUInteger columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; if (tableView == self.paletteTableView) { if (columnIndex == 0) { - return [NSString stringWithFormat:@"%s %d", row >=8 ? "Object" : "Background", (int)(row & 7)]; + return [NSString stringWithFormat:@"%s %u", row >= 8 ? "Object" : "Background", (unsigned)(row & 7)]; } uint8_t *palette_data = GB_get_direct_access(&gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, NULL, NULL); @@ -1318,9 +1564,9 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, height:oamHeight scale:16.0/oamHeight]; case 1: - return @((int)oamInfo[row].x - 8); + return @((unsigned)oamInfo[row].x - 8); case 2: - return @((int)oamInfo[row].y - 16); + return @((unsigned)oamInfo[row].y - 16); case 3: return [NSString stringWithFormat:@"$%02x", oamInfo[row].tile]; case 4: @@ -1384,20 +1630,31 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, scale:2.0]; NSRect frame = self.printerFeedWindow.frame; 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 setMaxSize:frame.size]; [self.printerFeedWindow setFrame:frame display:NO animate: self.printerFeedWindow.isVisible]; [self.printerFeedWindow orderFront:NULL]; }); } + +- (void)printDocument:(id)sender +{ + if (self.feedImageView.image.size.height == 0) { + NSBeep(); return; + } + NSImageView *view = [[NSImageView alloc] initWithFrame:(NSRect){{0,0}, self.feedImageView.image.size}]; + view.image = self.feedImageView.image; + [[NSPrintOperation printOperationWithView:view] runOperationModalForWindow:self.printerFeedWindow delegate:nil didRunSelector:NULL contextInfo:NULL]; +} + - (IBAction)savePrinterFeed:(id)sender { bool shouldResume = running; [self stop]; NSSavePanel * savePanel = [NSSavePanel savePanel]; [savePanel setAllowedFileTypes:@[@"png"]]; - [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result){ + [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) { if (result == NSFileHandlingPanelOKButton) { [savePanel orderOut:self]; CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL @@ -1426,7 +1683,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, - (IBAction)connectPrinter:(id)sender { [self performAtomicBlock:^{ - accessory = GBAccessoryPrinter; + accessory = GBAccessoryPrinter; GB_connect_printer(&gb, printImage); }]; } @@ -1445,6 +1702,11 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } } +- (void) updateFrameBlendingMode +{ + self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; +} + - (void) updateRewindLength { [self performAtomicBlock:^{ @@ -1488,4 +1750,52 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } +- (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview; +{ + if ([[splitView arrangedSubviews] lastObject] == subview) { + return YES; + } + return NO; +} + +- (CGFloat)splitView:(GBSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex +{ + return 600; +} + +- (CGFloat)splitView:(GBSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex +{ + return splitView.frame.size.width - 321; +} + +- (BOOL)splitView:(GBSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view +{ + if ([[splitView arrangedSubviews] lastObject] == view) { + return NO; + } + return YES; +} + +- (void)splitViewDidResizeSubviews:(NSNotification *)notification +{ + GBSplitView *splitview = notification.object; + if ([[[splitview arrangedSubviews] firstObject] frame].size.width < 600) { + [splitview setPosition:600 ofDividerAtIndex:0]; + } + /* NSSplitView renders its separator without the proper vibrancy, so we made it transparent and move an + NSBox-based separator that renders properly so it acts like the split view's separator. */ + NSRect rect = self.debuggerVerticalLine.frame; + rect.origin.x = [[[splitview arrangedSubviews] firstObject] frame].size.width - 1; + self.debuggerVerticalLine.frame = rect; +} + +- (IBAction)showCheats:(id)sender +{ + [self.cheatsWindow makeKeyAndOrderFront:nil]; +} + +- (IBAction)toggleCheats:(id)sender +{ + GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb)); +} @end diff --git a/Cocoa/Document.xib b/Cocoa/Document.xib index 88c5275..1197c0f 100644 --- a/Cocoa/Document.xib +++ b/Cocoa/Document.xib @@ -1,21 +1,24 @@ - + - + + + + + - @@ -39,22 +42,21 @@ - + - - + - + - + @@ -67,49 +69,17 @@ - + - - + - - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - + @@ -124,91 +94,147 @@ - + - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + - - + + - - + - + @@ -217,9 +243,9 @@ - + - + @@ -243,12 +269,12 @@ - + - + @@ -260,12 +286,12 @@ - + - + @@ -284,21 +310,20 @@ - - + + - - + - + - + - + @@ -307,17 +332,17 @@ - + - + - + @@ -326,7 +351,7 @@ - + @@ -358,7 +383,7 @@ - + @@ -386,8 +422,8 @@ - - + + @@ -419,8 +455,8 @@ - - + + @@ -437,8 +473,8 @@ - - + + @@ -463,14 +499,14 @@ - + - + - + @@ -586,11 +622,11 @@ - - - - - + + + + - + - + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + - - + + + + + diff --git a/Cocoa/GBAudioClient.m b/Cocoa/GBAudioClient.m index 81ddec4..7f2115d 100644 --- a/Cocoa/GBAudioClient.m +++ b/Cocoa/GBAudioClient.m @@ -26,8 +26,7 @@ static OSStatus render( -(id) initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block andSampleRate:(UInt32) rate { - if(!(self = [super init])) - { + if (!(self = [super init])) { return nil; } @@ -102,7 +101,8 @@ static OSStatus render( _playing = NO; } --(void) dealloc { +-(void) dealloc +{ [self stop]; AudioUnitUninitialize(audioUnit); AudioComponentInstanceDispose(audioUnit); diff --git a/Cocoa/GBButtons.h b/Cocoa/GBButtons.h index 7b2ea5d..1f8b5af 100644 --- a/Cocoa/GBButtons.h +++ b/Cocoa/GBButtons.h @@ -19,6 +19,11 @@ typedef enum : NSUInteger { extern NSString const *GBButtonNames[GBButtonCount]; +static inline NSString *n2s(uint64_t number) +{ + return [NSString stringWithFormat:@"%llx", number]; +} + static inline NSString *button_to_preference_name(GBButton button, unsigned player) { if (player) { diff --git a/Cocoa/GBCheatTextFieldCell.h b/Cocoa/GBCheatTextFieldCell.h new file mode 100644 index 0000000..473e0f3 --- /dev/null +++ b/Cocoa/GBCheatTextFieldCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBCheatTextFieldCell : NSTextFieldCell +@property bool usesAddressFormat; +@end diff --git a/Cocoa/GBCheatTextFieldCell.m b/Cocoa/GBCheatTextFieldCell.m new file mode 100644 index 0000000..611cade --- /dev/null +++ b/Cocoa/GBCheatTextFieldCell.m @@ -0,0 +1,121 @@ +#import "GBCheatTextFieldCell.h" + +@interface GBCheatTextView : NSTextView +@property bool usesAddressFormat; +@end + +@implementation GBCheatTextView + +- (bool)_insertText:(NSString *)string replacementRange:(NSRange)range +{ + if (range.location == NSNotFound) { + range = self.selectedRange; + } + + NSString *new = [self.string stringByReplacingCharactersInRange:range withString:string]; + if (!self.usesAddressFormat) { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^(\\$[0-9A-Fa-f]{1,2}|[0-9]{1,3})$" options:0 error:NULL]; + if ([regex numberOfMatchesInString:new options:0 range:NSMakeRange(0, new.length)]) { + [super insertText:string replacementRange:range]; + return true; + } + if ([regex numberOfMatchesInString:[@"$" stringByAppendingString:new] options:0 range:NSMakeRange(0, new.length + 1)]) { + [super insertText:string replacementRange:range]; + [super insertText:@"$" replacementRange:NSMakeRange(0, 0)]; + return true; + } + if ([new isEqualToString:@"$"] || [string length] == 0) { + self.string = @"$00"; + self.selectedRange = NSMakeRange(1, 2); + return true; + } + } + else { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^(\\$[0-9A-Fa-f]{1,3}:)?\\$[0-9a-fA-F]{1,4}$" options:0 error:NULL]; + if ([regex numberOfMatchesInString:new options:0 range:NSMakeRange(0, new.length)]) { + [super insertText:string replacementRange:range]; + return true; + } + if ([string length] == 0) { + NSUInteger index = [new rangeOfString:@":"].location; + if (index != NSNotFound) { + if (range.location > index) { + self.string = [[new componentsSeparatedByString:@":"] firstObject]; + self.selectedRange = NSMakeRange(self.string.length, 0); + return true; + } + self.string = [[new componentsSeparatedByString:@":"] lastObject]; + self.selectedRange = NSMakeRange(0, 0); + return true; + } + else if ([[self.string substringWithRange:range] isEqualToString:@":"]) { + self.string = [[self.string componentsSeparatedByString:@":"] lastObject]; + self.selectedRange = NSMakeRange(0, 0); + return true; + } + } + if ([new isEqualToString:@"$"] || [string length] == 0) { + self.string = @"$0000"; + self.selectedRange = NSMakeRange(1, 4); + return true; + } + if (([string isEqualToString:@"$"] || [string isEqualToString:@":"]) && range.length == 0 && range.location == 0) { + if ([self _insertText:@"$00:" replacementRange:range]) { + self.selectedRange = NSMakeRange(1, 2); + return true; + } + } + if ([string isEqualToString:@":"] && range.length + range.location == self.string.length) { + if ([self _insertText:@":$0" replacementRange:range]) { + self.selectedRange = NSMakeRange(self.string.length - 2, 2); + return true; + } + } + if ([string isEqualToString:@"$"]) { + if ([self _insertText:@"$0" replacementRange:range]) { + self.selectedRange = NSMakeRange(range.location + 1, 1); + return true; + } + } + } + return false; +} + +- (NSUndoManager *)undoManager +{ + return nil; +} + +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange +{ + if (![self _insertText:string replacementRange:replacementRange]) { + NSBeep(); + } +} + +/* Private API, don't tell the police! */ +- (void)_userReplaceRange:(NSRange)range withString:(NSString *)string +{ + [self insertText:string replacementRange:range]; +} + +@end + +@implementation GBCheatTextFieldCell +{ + bool _drawing, _editing; + GBCheatTextView *_fieldEditor; +} + +- (NSTextView *)fieldEditorForView:(NSView *)controlView +{ + if (_fieldEditor) { + _fieldEditor.usesAddressFormat = self.usesAddressFormat; + return _fieldEditor; + } + _fieldEditor = [[GBCheatTextView alloc] initWithFrame:controlView.frame]; + _fieldEditor.fieldEditor = YES; + _fieldEditor.usesAddressFormat = self.usesAddressFormat; + return _fieldEditor; +} +@end diff --git a/Cocoa/GBCheatWindowController.h b/Cocoa/GBCheatWindowController.h new file mode 100644 index 0000000..f70553e --- /dev/null +++ b/Cocoa/GBCheatWindowController.h @@ -0,0 +1,17 @@ +#import +#import +#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; +- (void)cheatsUpdated; +@end + diff --git a/Cocoa/GBCheatWindowController.m b/Cocoa/GBCheatWindowController.m new file mode 100644 index 0000000..c10e2a9 --- /dev/null +++ b/Cocoa/GBCheatWindowController.m @@ -0,0 +1,240 @@ +#import "GBCheatWindowController.h" +#import "GBWarningPopover.h" +#import "GBCheatTextFieldCell.h" + +@implementation GBCheatWindowController + ++ (NSString *)addressStringFromCheat:(const GB_cheat_t *)cheat +{ + if (cheat->bank != GB_CHEAT_ANY_BANK) { + return [NSString stringWithFormat:@"$%x:$%04x", cheat->bank, cheat->address]; + } + return [NSString stringWithFormat:@"$%04x", cheat->address]; +} + ++ (NSString *)actionDescriptionForCheat:(const GB_cheat_t *)cheat +{ + if (cheat->use_old_value) { + return [NSString stringWithFormat:@"[%@]($%02x) = $%02x", [self addressStringFromCheat:cheat], cheat->old_value, cheat->value]; + } + return [NSString stringWithFormat:@"[%@] = $%02x", [self addressStringFromCheat:cheat], cheat->value]; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + GB_gameboy_t *gb = self.document.gameboy; + if (!gb) return 0; + size_t cheatCount; + GB_get_cheats(gb, &cheatCount); + return cheatCount + 1; +} + +- (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + GB_gameboy_t *gb = self.document.gameboy; + if (!gb) return nil; + size_t cheatCount; + GB_get_cheats(gb, &cheatCount); + NSUInteger columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + if (row >= cheatCount && columnIndex == 0) { + return [[NSCell alloc] init]; + } + return nil; +} + +- (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row +{ + size_t cheatCount; + GB_gameboy_t *gb = self.document.gameboy; + if (!gb) return nil; + const GB_cheat_t *const *cheats = GB_get_cheats(gb, &cheatCount); + NSUInteger columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + if (row >= cheatCount) { + switch (columnIndex) { + case 0: + return @(YES); + + case 1: + return @NO; + + case 2: + return @"Add Cheat..."; + + case 3: + return @""; + } + } + + switch (columnIndex) { + case 0: + return @(NO); + + case 1: + return @(cheats[row]->enabled); + + case 2: + return @(cheats[row]->description); + + case 3: + return [GBCheatWindowController actionDescriptionForCheat:cheats[row]]; + } + + return nil; +} + +- (IBAction)importCheat:(id)sender +{ + GB_gameboy_t *gb = self.document.gameboy; + if (!gb) return; + + [self.document performAtomicBlock:^{ + if (GB_import_cheat(gb, + self.importCodeField.stringValue.UTF8String, + self.importDescriptionField.stringValue.UTF8String, + true)) { + self.importCodeField.stringValue = @""; + self.importDescriptionField.stringValue = @""; + [self.cheatsTable reloadData]; + [self tableViewSelectionDidChange:nil]; + } + else { + NSBeep(); + [GBWarningPopover popoverWithContents:@"This code is not a valid GameShark or GameGenie code" onView:self.importCodeField]; + } + }]; +} + +- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + GB_gameboy_t *gb = self.document.gameboy; + if (!gb) return; + size_t cheatCount; + const GB_cheat_t *const *cheats = GB_get_cheats(gb, &cheatCount); + NSUInteger columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + [self.document performAtomicBlock:^{ + if (columnIndex == 1) { + if (row >= cheatCount) { + GB_add_cheat(gb, "New Cheat", 0, 0, 0, 0, false, true); + } + else { + GB_update_cheat(gb, cheats[row], cheats[row]->description, cheats[row]->address, cheats[row]->bank, cheats[row]->value, cheats[row]->old_value, cheats[row]->use_old_value, !cheats[row]->enabled); + } + } + else if (row < cheatCount) { + GB_remove_cheat(gb, cheats[row]); + } + }]; + [self.cheatsTable reloadData]; + [self tableViewSelectionDidChange:nil]; +} + +- (void)tableViewSelectionDidChange:(NSNotification *)notification +{ + GB_gameboy_t *gb = self.document.gameboy; + if (!gb) return; + + size_t cheatCount; + const GB_cheat_t *const *cheats = GB_get_cheats(gb, &cheatCount); + unsigned row = self.cheatsTable.selectedRow; + const GB_cheat_t *cheat = NULL; + if (row >= cheatCount) { + static const GB_cheat_t template = { + .address = 0, + .bank = 0, + .value = 0, + .old_value = 0, + .use_old_value = false, + .enabled = false, + .description = "New Cheat", + }; + cheat = &template; + } + else { + cheat = cheats[row]; + } + + self.addressField.stringValue = [GBCheatWindowController addressStringFromCheat:cheat]; + self.valueField.stringValue = [NSString stringWithFormat:@"$%02x", cheat->value]; + self.oldValueField.stringValue = [NSString stringWithFormat:@"$%02x", cheat->old_value]; + self.oldValueCheckbox.state = cheat->use_old_value; + self.descriptionField.stringValue = @(cheat->description); +} + +- (void)awakeFromNib +{ + [self tableViewSelectionDidChange:nil]; + ((GBCheatTextFieldCell *)self.addressField.cell).usesAddressFormat = true; +} + +- (void)controlTextDidChange:(NSNotification *)obj +{ + [self updateCheat:nil]; +} + +- (IBAction)updateCheat:(id)sender +{ + GB_gameboy_t *gb = self.document.gameboy; + if (!gb) return; + + uint16_t address = 0; + uint16_t bank = GB_CHEAT_ANY_BANK; + if ([self.addressField.stringValue rangeOfString:@":"].location != NSNotFound) { + sscanf(self.addressField.stringValue.UTF8String, "$%hx:$%hx", &bank, &address); + } + else { + sscanf(self.addressField.stringValue.UTF8String, "$%hx", &address); + } + + uint8_t value = 0; + if ([self.valueField.stringValue characterAtIndex:0] == '$') { + sscanf(self.valueField.stringValue.UTF8String, "$%02hhx", &value); + } + else { + sscanf(self.valueField.stringValue.UTF8String, "%hhd", &value); + } + + uint8_t oldValue = 0; + if ([self.oldValueField.stringValue characterAtIndex:0] == '$') { + sscanf(self.oldValueField.stringValue.UTF8String, "$%02hhx", &oldValue); + } + else { + sscanf(self.oldValueField.stringValue.UTF8String, "%hhd", &oldValue); + } + + size_t cheatCount; + const GB_cheat_t *const *cheats = GB_get_cheats(gb, &cheatCount); + unsigned row = self.cheatsTable.selectedRow; + + [self.document performAtomicBlock:^{ + if (row >= cheatCount) { + GB_add_cheat(gb, + self.descriptionField.stringValue.UTF8String, + address, + bank, + value, + oldValue, + self.oldValueCheckbox.state, + false); + } + else { + GB_update_cheat(gb, + cheats[row], + self.descriptionField.stringValue.UTF8String, + address, + bank, + value, + oldValue, + self.oldValueCheckbox.state, + cheats[row]->enabled); + } + }]; + [self.cheatsTable reloadData]; +} + +- (void)cheatsUpdated +{ + [self.cheatsTable reloadData]; + [self tableViewSelectionDidChange:nil]; +} + +@end diff --git a/Cocoa/GBColorCell.m b/Cocoa/GBColorCell.m index afbbc8d..0ad102a 100644 --- a/Cocoa/GBColorCell.m +++ b/Cocoa/GBColorCell.m @@ -13,8 +13,14 @@ static inline double scale_channel(uint8_t x) - (void)setObjectValue:(id)objectValue { + _integerValue = [objectValue integerValue]; - super.objectValue = [NSString stringWithFormat:@"$%04x", (uint16_t)(_integerValue & 0x7FFF)]; + 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] + }]; } - (NSInteger)integerValue diff --git a/Cocoa/GBGLShader.h b/Cocoa/GBGLShader.h index 1a12617..8e46f93 100644 --- a/Cocoa/GBGLShader.h +++ b/Cocoa/GBGLShader.h @@ -1,6 +1,7 @@ #import +#import "GBView.h" @interface GBGLShader : NSObject - (instancetype)initWithName:(NSString *) shaderName; -- (void) renderBitmap: (void *)bitmap previous:(void*) previous sized:(NSSize)srcSize inSize:(NSSize)dstSize scale: (double) scale; +- (void) renderBitmap: (void *)bitmap previous:(void*) previous sized:(NSSize)srcSize inSize:(NSSize)dstSize scale: (double) scale withBlendingMode: (GB_frame_blending_mode_t)blendingMode; @end diff --git a/Cocoa/GBGLShader.m b/Cocoa/GBGLShader.m index fe636f8..920226b 100644 --- a/Cocoa/GBGLShader.m +++ b/Cocoa/GBGLShader.m @@ -21,7 +21,7 @@ void main(void) {\n\ GLuint resolution_uniform; GLuint texture_uniform; GLuint previous_texture_uniform; - GLuint mix_previous_uniform; + GLuint frame_blending_mode_uniform; GLuint position_attribute; GLuint texture; @@ -70,7 +70,7 @@ void main(void) {\n\ glBindTexture(GL_TEXTURE_2D, 0); previous_texture_uniform = glGetUniformLocation(program, "previous_image"); - mix_previous_uniform = glGetUniformLocation(program, "mix_previous"); + frame_blending_mode_uniform = glGetUniformLocation(program, "frame_blending_mode"); // Configure OpenGL [self configureOpenGL]; @@ -79,7 +79,7 @@ void main(void) {\n\ return self; } -- (void) renderBitmap: (void *)bitmap previous:(void*) previous sized:(NSSize)srcSize inSize:(NSSize)dstSize scale: (double) scale +- (void) renderBitmap: (void *)bitmap previous:(void*) previous sized:(NSSize)srcSize inSize:(NSSize)dstSize scale: (double) scale withBlendingMode:(GB_frame_blending_mode_t)blendingMode { glUseProgram(program); glUniform2f(resolution_uniform, dstSize.width * scale, dstSize.height * scale); @@ -87,8 +87,8 @@ void main(void) {\n\ glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, srcSize.width, srcSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitmap); glUniform1i(texture_uniform, 0); - glUniform1i(mix_previous_uniform, previous != NULL); - if (previous) { + glUniform1i(frame_blending_mode_uniform, blendingMode); + if (blendingMode) { glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, previous_texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, srcSize.width, srcSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, previous); @@ -169,7 +169,7 @@ void main(void) {\n\ + (GLuint)shaderWithContents:(NSString*)contents type:(GLenum)type { - const GLchar* source = [contents UTF8String]; + const GLchar *source = [contents UTF8String]; // Create the shader object GLuint shader = glCreateShader(type); // Load the shader source diff --git a/Cocoa/GBImageCell.m b/Cocoa/GBImageCell.m index 6f54ec8..de75e0e 100644 --- a/Cocoa/GBImageCell.m +++ b/Cocoa/GBImageCell.m @@ -3,7 +3,7 @@ @implementation GBImageCell - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { - CGContextRef context = [[NSGraphicsContext currentContext] CGContext]; + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; CGContextSetInterpolationQuality(context, kCGInterpolationNone); [super drawWithFrame:cellFrame inView:controlView]; } diff --git a/Cocoa/GBImageView.h b/Cocoa/GBImageView.h index 22a8829..d5ee534 100644 --- a/Cocoa/GBImageView.h +++ b/Cocoa/GBImageView.h @@ -11,7 +11,9 @@ @interface GBImageView : NSImageView @property (nonatomic) NSArray *horizontalGrids; @property (nonatomic) NSArray *verticalGrids; -@property (weak) IBOutlet id delegate; +@property (nonatomic) bool displayScrollRect; +@property NSRect scrollRect; +@property (weak) IBOutlet id delegate; @end @protocol GBImageViewDelegate diff --git a/Cocoa/GBImageView.m b/Cocoa/GBImageView.m index 674d4b2..3525e72 100644 --- a/Cocoa/GBImageView.m +++ b/Cocoa/GBImageView.m @@ -16,7 +16,7 @@ } - (void)drawRect:(NSRect)dirtyRect { - CGContextRef context = [[NSGraphicsContext currentContext] CGContext]; + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; CGContextSetInterpolationQuality(context, kCGInterpolationNone); [super drawRect:dirtyRect]; CGFloat y_ratio = self.frame.size.height / self.image.size.height; @@ -25,8 +25,8 @@ [conf.color set]; for (CGFloat y = conf.size * y_ratio; y < self.frame.size.height; y += conf.size * y_ratio) { NSBezierPath *line = [NSBezierPath bezierPath]; - [line moveToPoint:NSMakePoint(0, y + 0.5)]; - [line lineToPoint:NSMakePoint(self.frame.size.width, y + 0.5)]; + [line moveToPoint:NSMakePoint(0, y - 0.5)]; + [line lineToPoint:NSMakePoint(self.frame.size.width, y - 0.5)]; [line setLineWidth:1.0]; [line stroke]; } @@ -42,6 +42,35 @@ [line stroke]; } } + + if (self.displayScrollRect) { + NSBezierPath *path = [NSBezierPath bezierPathWithRect:CGRectInfinite]; + for (unsigned x = 0; x < 2; x++) { + for (unsigned y = 0; y < 2; y++) { + NSRect rect = self.scrollRect; + rect.origin.x *= x_ratio; + rect.origin.y *= y_ratio; + rect.size.width *= x_ratio; + rect.size.height *= y_ratio; + rect.origin.y = self.frame.size.height - rect.origin.y - rect.size.height; + + rect.origin.x -= self.frame.size.width * x; + rect.origin.y += self.frame.size.height * y; + + + NSBezierPath *subpath = [NSBezierPath bezierPathWithRect:rect]; + [path appendBezierPath:subpath]; + } + } + [path setWindingRule:NSEvenOddWindingRule]; + [path setLineWidth:4.0]; + [path setLineJoinStyle:NSRoundLineJoinStyle]; + [[NSColor colorWithWhite:0.2 alpha:0.5] set]; + [path fill]; + [path addClip]; + [[NSColor colorWithWhite:0.0 alpha:0.6] set]; + [path stroke]; + } } - (void)setHorizontalGrids:(NSArray *)horizontalGrids @@ -56,9 +85,15 @@ [self setNeedsDisplay]; } +- (void)setDisplayScrollRect:(bool)displayScrollRect +{ + self->_displayScrollRect = displayScrollRect; + [self setNeedsDisplay]; +} + - (void)updateTrackingAreas { - if(trackingArea != nil) { + if (trackingArea != nil) { [self removeTrackingArea:trackingArea]; } diff --git a/Cocoa/GBJoystickListener.h b/Cocoa/GBJoystickListener.h deleted file mode 100644 index 690fde9..0000000 --- a/Cocoa/GBJoystickListener.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -@protocol GBJoystickListener - -- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state; -- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value; - -@end diff --git a/Cocoa/GBOpenGLView.m b/Cocoa/GBOpenGLView.m index 67a9f8d..90ebf8d 100644 --- a/Cocoa/GBOpenGLView.m +++ b/Cocoa/GBOpenGLView.m @@ -4,7 +4,8 @@ @implementation GBOpenGLView -- (void)drawRect:(NSRect)dirtyRect { +- (void)drawRect:(NSRect)dirtyRect +{ if (!self.shader) { self.shader = [[GBGLShader alloc] initWithName:[[NSUserDefaults standardUserDefaults] objectForKey:@"GBFilter"]]; } @@ -13,11 +14,14 @@ double scale = self.window.backingScaleFactor; glViewport(0, 0, self.bounds.size.width * scale, self.bounds.size.height * scale); - [self.shader renderBitmap:gbview.currentBuffer - previous:gbview.shouldBlendFrameWithPrevious? gbview.previousBuffer : NULL - sized:NSMakeSize(GB_get_screen_width(gbview.gb), GB_get_screen_height(gbview.gb)) - inSize:self.bounds.size - scale:scale]; + if (gbview.gb) { + [self.shader renderBitmap:gbview.currentBuffer + previous:gbview.frameBlendingMode? gbview.previousBuffer : NULL + sized:NSMakeSize(GB_get_screen_width(gbview.gb), GB_get_screen_height(gbview.gb)) + inSize:self.bounds.size + scale:scale + withBlendingMode:gbview.frameBlendingMode]; + } glFlush(); } diff --git a/Cocoa/GBOptionalVisualEffectView.h b/Cocoa/GBOptionalVisualEffectView.h new file mode 100644 index 0000000..1355071 --- /dev/null +++ b/Cocoa/GBOptionalVisualEffectView.h @@ -0,0 +1,6 @@ +#import + +/* Fake interface so the compiler assumes it conforms to NSVisualEffectView */ +@interface GBOptionalVisualEffectView : NSVisualEffectView + +@end diff --git a/Cocoa/GBOptionalVisualEffectView.m b/Cocoa/GBOptionalVisualEffectView.m new file mode 100644 index 0000000..c28eb59 --- /dev/null +++ b/Cocoa/GBOptionalVisualEffectView.m @@ -0,0 +1,18 @@ +#import + +@interface GBOptionalVisualEffectView : NSView + +@end + +@implementation GBOptionalVisualEffectView + ++ (instancetype)allocWithZone:(struct _NSZone *)zone +{ + Class NSVisualEffectView = NSClassFromString(@"NSVisualEffectView"); + if (NSVisualEffectView) { + return (id)[NSVisualEffectView alloc]; + } + return [super allocWithZone:zone]; +} + +@end diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h index 90eee54..ee697a8 100644 --- a/Cocoa/GBPreferencesWindow.h +++ b/Cocoa/GBPreferencesWindow.h @@ -1,17 +1,22 @@ #import -#import "GBJoystickListener.h" +#import -@interface GBPreferencesWindow : NSWindow +@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; diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index 982aa45..aa219d8 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -9,17 +9,22 @@ NSInteger button_being_modified; signed joystick_configuration_state; NSString *joystick_being_configured; - signed last_axis; + bool joypad_wait; NSPopUpButton *_graphicsFilterPopupButton; NSPopUpButton *_highpassFilterPopupButton; NSPopUpButton *_colorCorrectionPopupButton; + NSPopUpButton *_frameBlendingModePopupButton; + NSPopUpButton *_colorPalettePopupButton; + NSPopUpButton *_displayBorderPopupButton; NSPopUpButton *_rewindPopupButton; NSButton *_aspectRatioCheckbox; + NSButton *_analogControlsCheckbox; NSEventModifierFlags previousModifiers; NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton; NSPopUpButton *_preferredJoypadButton; + NSPopUpButton *_rumbleModePopupButton; } + (NSArray *)filterList @@ -31,6 +36,7 @@ @"NearestNeighbor", @"Bilinear", @"SmoothBilinear", + @"MonoLCD", @"LCD", @"CRT", @"Scale2x", @@ -51,7 +57,7 @@ joystick_configuration_state = -1; [self.configureJoypadButton setEnabled:YES]; [self.skipButton setEnabled:NO]; - [self.configureJoypadButton setTitle:@"Configure Joypad"]; + [self.configureJoypadButton setTitle:@"Configure Controller"]; [super close]; } @@ -84,6 +90,54 @@ return _colorCorrectionPopupButton; } +- (void)setFrameBlendingModePopupButton:(NSPopUpButton *)frameBlendingModePopupButton +{ + _frameBlendingModePopupButton = frameBlendingModePopupButton; + NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; + [_frameBlendingModePopupButton selectItemAtIndex:mode]; +} + +- (NSPopUpButton *)frameBlendingModePopupButton +{ + return _frameBlendingModePopupButton; +} + +- (void)setColorPalettePopupButton:(NSPopUpButton *)colorPalettePopupButton +{ + _colorPalettePopupButton = colorPalettePopupButton; + NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]; + [_colorPalettePopupButton selectItemAtIndex:mode]; +} + +- (NSPopUpButton *)colorPalettePopupButton +{ + return _colorPalettePopupButton; +} + +- (void)setDisplayBorderPopupButton:(NSPopUpButton *)displayBorderPopupButton +{ + _displayBorderPopupButton = displayBorderPopupButton; + NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]; + [_displayBorderPopupButton selectItemWithTag:mode]; +} + +- (NSPopUpButton *)displayBorderPopupButton +{ + return _displayBorderPopupButton; +} + +- (void)setRumbleModePopupButton:(NSPopUpButton *)rumbleModePopupButton +{ + _rumbleModePopupButton = rumbleModePopupButton; + NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRumbleMode"]; + [_rumbleModePopupButton selectItemWithTag:mode]; +} + +- (NSPopUpButton *)rumbleModePopupButton +{ + return _rumbleModePopupButton; +} + - (void)setRewindPopupButton:(NSPopUpButton *)rewindPopupButton { _rewindPopupButton = rewindPopupButton; @@ -110,6 +164,20 @@ return GBGameBoyButtonCount; } +- (unsigned) usesForKey:(unsigned) key +{ + unsigned ret = 0; + for (unsigned player = 4; player--;) { + for (unsigned button = player == 0? GBButtonCount:GBGameBoyButtonCount; button--;) { + NSNumber *other = [[NSUserDefaults standardUserDefaults] valueForKey:button_to_preference_name(button, player)]; + if (other && [other unsignedIntValue] == key) { + ret++; + } + } + } + return ret; +} + - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { if ([tableColumn.identifier isEqualToString:@"keyName"]) { @@ -122,6 +190,12 @@ NSNumber *key = [[NSUserDefaults standardUserDefaults] valueForKey:button_to_preference_name(row, self.playerListButton.selectedTag)]; if (key) { + if ([self usesForKey:[key unsignedIntValue]] > 1) { + return [[NSAttributedString alloc] initWithString:[NSString displayStringForKeyCode: [key unsignedIntegerValue]] + attributes:@{NSForegroundColorAttributeName: [NSColor colorWithRed:0.9375 green:0.25 blue:0.25 alpha:1.0], + NSFontAttributeName: [NSFont boldSystemFontOfSize:[NSFont systemFontSize]] + }]; + } return [NSString displayStringForKeyCode: [key unsignedIntegerValue]]; } @@ -184,6 +258,12 @@ [[NSNotificationCenter defaultCenter] postNotificationName:@"GBHighpassFilterChanged" object:nil]; } +- (IBAction)changeAnalogControls:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState + forKey:@"GBAnalogControls"]; +} + - (IBAction)changeAspectRatio:(id)sender { [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] != NSOnState @@ -196,7 +276,35 @@ [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) forKey:@"GBColorCorrection"]; [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorCorrectionChanged" object:nil]; +} +- (IBAction)franeBlendingModeChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) + forKey:@"GBFrameBlendingMode"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFrameBlendingModeChanged" object:nil]; + +} + +- (IBAction)colorPaletteChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) + forKey:@"GBColorPalette"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorPaletteChanged" object:nil]; +} + +- (IBAction)displayBorderChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedItem].tag) + forKey:@"GBBorderMode"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBBorderModeChanged" object:nil]; +} + +- (IBAction)rumbleModeChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedItem].tag) + forKey:@"GBRumbleMode"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRumbleModeChanged" object:nil]; } - (IBAction)rewindLengthChanged:(id)sender @@ -212,7 +320,6 @@ [self.skipButton setEnabled:YES]; joystick_being_configured = nil; [self advanceConfigurationStateMachine]; - last_axis = -1; } - (IBAction) skipButton:(id)sender @@ -223,11 +330,11 @@ - (void) advanceConfigurationStateMachine { joystick_configuration_state++; - if (joystick_configuration_state < GBButtonCount) { - [self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]]; + if (joystick_configuration_state == GBUnderclock) { + [self.configureJoypadButton setTitle:@"Press Button for Slo-Mo"]; // Full name is too long :< } - else if (joystick_configuration_state == GBButtonCount) { - [self.configureJoypadButton setTitle:@"Move the Analog Stick"]; + else if (joystick_configuration_state < GBButtonCount) { + [self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]]; } else { joystick_configuration_state = -1; @@ -237,76 +344,95 @@ } } -- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button { - if (!state) return; + /* Debounce */ + if (joypad_wait) return; + joypad_wait = true; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + joypad_wait = false; + }); + + if (!button.isPressed) return; if (joystick_configuration_state == -1) return; if (joystick_configuration_state == GBButtonCount) return; if (!joystick_being_configured) { - joystick_being_configured = joystick_name; + joystick_being_configured = controller.uniqueID; } - else if (![joystick_being_configured isEqualToString:joystick_name]) { + else if (![joystick_being_configured isEqualToString:controller.uniqueID]) { return; } - NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy]; + NSMutableDictionary *instance_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"] mutableCopy]; - if (!all_mappings) { - all_mappings = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *name_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"] mutableCopy]; + + + if (!instance_mappings) { + instance_mappings = [[NSMutableDictionary alloc] init]; } - NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy]; + if (!name_mappings) { + name_mappings = [[NSMutableDictionary alloc] init]; + } - if (!mapping) { + NSMutableDictionary *mapping = nil; + if (joystick_configuration_state != 0) { + mapping = [instance_mappings[controller.uniqueID] mutableCopy]; + } + else { mapping = [[NSMutableDictionary alloc] init]; } + - mapping[GBButtonNames[joystick_configuration_state]] = @(button); + 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, + }; - all_mappings[joystick_name] = mapping; - [[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"]; - [self refreshJoypadMenu:nil]; + if (joystick_configuration_state == GBUnderclock) { + for (JOYAxis *axis in controller.axes) { + if (axis.value > 0.5) { + mapping[@"AnalogUnderclock"] = @(axis.uniqueID); + } + } + } + + if (joystick_configuration_state == GBTurbo) { + for (JOYAxis *axis in controller.axes) { + if (axis.value > 0.5) { + mapping[@"AnalogTurbo"] = @(axis.uniqueID); + } + } + } + + mapping[n2s(button.uniqueID)] = @(gb_to_joykit[joystick_configuration_state]); + + instance_mappings[controller.uniqueID] = mapping; + name_mappings[controller.deviceName] = mapping; + [[NSUserDefaults standardUserDefaults] setObject:instance_mappings forKey:@"JoyKitInstanceMapping"]; + [[NSUserDefaults standardUserDefaults] setObject:name_mappings forKey:@"JoyKitNameMapping"]; [self advanceConfigurationStateMachine]; } -- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value +- (NSButton *)analogControlsCheckbox { - if (abs(value) < 0x4000) return; - if (joystick_configuration_state != GBButtonCount) return; - if (!joystick_being_configured) { - joystick_being_configured = joystick_name; - } - else if (![joystick_being_configured isEqualToString:joystick_name]) { - return; - } - - if (last_axis == -1) { - last_axis = axis; - return; - } - - if (axis == last_axis) { - return; - } - - NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy]; - - if (!all_mappings) { - all_mappings = [[NSMutableDictionary alloc] init]; - } - - NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy]; - - if (!mapping) { - mapping = [[NSMutableDictionary alloc] init]; - } - - mapping[@"XAxis"] = @(MIN(axis, last_axis)); - mapping[@"YAxis"] = @(MAX(axis, last_axis)); - - all_mappings[joystick_name] = mapping; - [[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"]; - [self advanceConfigurationStateMachine]; + return _analogControlsCheckbox; +} + +- (void)setAnalogControlsCheckbox:(NSButton *)analogControlsCheckbox +{ + _analogControlsCheckbox = analogControlsCheckbox; + [_analogControlsCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]]; } - (NSButton *)aspectRatioCheckbox @@ -325,10 +451,13 @@ [super awakeFromNib]; [self updateBootROMFolderButton]; [[NSDistributedNotificationCenter defaultCenter] addObserver:self.controlsTableView selector:@selector(reloadData) name:(NSString*)kTISNotifySelectedKeyboardInputSourceChanged object:nil]; + [JOYController registerListener:self]; + joystick_configuration_state = -1; } - (void)dealloc { + [JOYController unregisterListener:self]; [[NSDistributedNotificationCenter defaultCenter] removeObserver:self.controlsTableView]; } @@ -447,21 +576,47 @@ return _preferredJoypadButton; } +- (void)controllerConnected:(JOYController *)controller +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self refreshJoypadMenu:nil]; + }); +} + +- (void)controllerDisconnected:(JOYController *)controller +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self refreshJoypadMenu:nil]; + }); +} + - (IBAction)refreshJoypadMenu:(id)sender { - NSArray *joypads = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] allKeys]; - for (NSString *joypad in joypads) { - if ([self.preferredJoypadButton indexOfItemWithTitle:joypad] == -1) { - [self.preferredJoypadButton addItemWithTitle:joypad]; + bool preferred_is_connected = false; + NSString *player_string = n2s(self.playerListButton.selectedTag); + NSString *selected_controller = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"][player_string]; + + [self.preferredJoypadButton removeAllItems]; + [self.preferredJoypadButton addItemWithTitle:@"None"]; + for (JOYController *controller in [JOYController allControllers]) { + [self.preferredJoypadButton addItemWithTitle:[NSString stringWithFormat:@"%@ (%@)", controller.deviceName, controller.uniqueID]]; + + self.preferredJoypadButton.lastItem.identifier = controller.uniqueID; + + if ([controller.uniqueID isEqualToString:selected_controller]) { + preferred_is_connected = true; + [self.preferredJoypadButton selectItem:self.preferredJoypadButton.lastItem]; } } - NSString *player_string = [NSString stringWithFormat: @"%ld", (long)self.playerListButton.selectedTag]; - NSString *selected_joypad = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"][player_string]; - if (selected_joypad && [self.preferredJoypadButton indexOfItemWithTitle:selected_joypad] != -1) { - [self.preferredJoypadButton selectItemWithTitle:selected_joypad]; + if (!preferred_is_connected && selected_controller) { + [self.preferredJoypadButton addItemWithTitle:[NSString stringWithFormat:@"Unavailable Controller (%@)", selected_controller]]; + self.preferredJoypadButton.lastItem.identifier = selected_controller; + [self.preferredJoypadButton selectItem:self.preferredJoypadButton.lastItem]; } - else { + + + if (!selected_controller) { [self.preferredJoypadButton selectItemWithTitle:@"None"]; } [self.controlsTableView reloadData]; @@ -469,18 +624,18 @@ - (IBAction)changeDefaultJoypad:(id)sender { - NSMutableDictionary *default_joypads = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"] mutableCopy]; + NSMutableDictionary *default_joypads = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"] mutableCopy]; if (!default_joypads) { default_joypads = [[NSMutableDictionary alloc] init]; } - NSString *player_string = [NSString stringWithFormat: @"%ld", self.playerListButton.selectedTag]; + NSString *player_string = n2s(self.playerListButton.selectedTag); if ([[sender titleOfSelectedItem] isEqualToString:@"None"]) { [default_joypads removeObjectForKey:player_string]; } else { - default_joypads[player_string] = [sender titleOfSelectedItem]; + default_joypads[player_string] = [[sender selectedItem] identifier]; } - [[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"GBDefaultJoypads"]; + [[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"JoyKitDefaultControllers"]; } @end diff --git a/Cocoa/GBSplitView.h b/Cocoa/GBSplitView.h new file mode 100644 index 0000000..6ab97cf --- /dev/null +++ b/Cocoa/GBSplitView.h @@ -0,0 +1,7 @@ +#import + +@interface GBSplitView : NSSplitView + +-(void) setDividerColor:(NSColor *)color; +- (NSArray *)arrangedSubviews; +@end diff --git a/Cocoa/GBSplitView.m b/Cocoa/GBSplitView.m new file mode 100644 index 0000000..a56c24e --- /dev/null +++ b/Cocoa/GBSplitView.m @@ -0,0 +1,33 @@ +#import "GBSplitView.h" + +@implementation GBSplitView +{ + NSColor *_dividerColor; +} + +- (void)setDividerColor:(NSColor *)color +{ + _dividerColor = color; + [self setNeedsDisplay:YES]; +} + +- (NSColor *)dividerColor +{ + if (_dividerColor) { + return _dividerColor; + } + return [super dividerColor]; +} + +/* Mavericks comaptibility */ +- (NSArray *)arrangedSubviews +{ + if (@available(macOS 10.11, *)) { + return [super arrangedSubviews]; + } + else { + return [self subviews]; + } +} + +@end diff --git a/Cocoa/GBTerminalTextFieldCell.h b/Cocoa/GBTerminalTextFieldCell.h index eae02e5..484e0c3 100644 --- a/Cocoa/GBTerminalTextFieldCell.h +++ b/Cocoa/GBTerminalTextFieldCell.h @@ -1,5 +1,6 @@ #import +#include @interface GBTerminalTextFieldCell : NSTextFieldCell - +@property GB_gameboy_t *gb; @end diff --git a/Cocoa/GBTerminalTextFieldCell.m b/Cocoa/GBTerminalTextFieldCell.m index 47a3a35..c1ed203 100644 --- a/Cocoa/GBTerminalTextFieldCell.m +++ b/Cocoa/GBTerminalTextFieldCell.m @@ -2,6 +2,7 @@ #import "GBTerminalTextFieldCell.h" @interface GBTerminalTextView : NSTextView +@property GB_gameboy_t *gb; @end @implementation GBTerminalTextFieldCell @@ -12,10 +13,12 @@ - (NSTextView *)fieldEditorForView:(NSView *)controlView { if (field_editor) { + field_editor.gb = self.gb; return field_editor; } field_editor = [[GBTerminalTextView alloc] init]; [field_editor setFieldEditor:YES]; + field_editor.gb = self.gb; return field_editor; } @@ -26,6 +29,8 @@ NSMutableOrderedSet *lines; NSUInteger current_line; bool reverse_search_mode; + NSRange auto_complete_range; + uintptr_t auto_complete_context; } - (instancetype)init @@ -170,10 +175,12 @@ -(void)setSelectedRanges:(NSArray *)ranges affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag { reverse_search_mode = false; + auto_complete_context = 0; [super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag]; } -- (BOOL)resignFirstResponder { +- (BOOL)resignFirstResponder +{ reverse_search_mode = false; return [super resignFirstResponder]; } @@ -187,6 +194,38 @@ [attributes setObject:color forKey:NSForegroundColorAttributeName]; [[[NSAttributedString alloc] initWithString:@"Reverse search..." attributes:attributes] drawAtPoint:NSMakePoint(2, 0)]; } - } + +/* Todo: lazy design, make it use a delegate instead of having a gb reference*/ + +- (void)insertTab:(id)sender +{ + if (auto_complete_context == 0) { + NSRange selection = self.selectedRange; + if (selection.length) { + [self delete:nil]; + } + auto_complete_range = NSMakeRange(selection.location, 0); + } + char *substring = strdup([self.string substringToIndex:auto_complete_range.location].UTF8String); + uintptr_t context = auto_complete_context; + char *completion = GB_debugger_complete_substring(self.gb, substring, &context); + free(substring); + if (completion) { + NSString *ns_completion = @(completion); + free(completion); + if (!ns_completion) { + goto error; + } + self.selectedRange = auto_complete_range; + auto_complete_range.length = ns_completion.length; + [self replaceCharactersInRange:self.selectedRange withString:ns_completion]; + auto_complete_context = context; + return; + } +error: + auto_complete_context = context; + NSBeep(); +} + @end diff --git a/Cocoa/GBView.h b/Cocoa/GBView.h index f4c5e44..80721cd 100644 --- a/Cocoa/GBView.h +++ b/Cocoa/GBView.h @@ -1,12 +1,20 @@ #import #include -#import "GBJoystickListener.h" +#import -@interface GBView : NSView +typedef enum { + GB_FRAME_BLENDING_MODE_DISABLED, + GB_FRAME_BLENDING_MODE_SIMPLE, + GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_EVEN = GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_ODD, +} GB_frame_blending_mode_t; + +@interface GBView : NSView - (void) flip; - (uint32_t *) pixels; @property GB_gameboy_t *gb; -@property (nonatomic) BOOL shouldBlendFrameWithPrevious; +@property (nonatomic) GB_frame_blending_mode_t frameBlendingMode; @property (getter=isMouseHidingEnabled) BOOL mouseHidingEnabled; @property bool isRewinding; @property NSView *internalView; @@ -14,4 +22,5 @@ - (uint32_t *)currentBuffer; - (uint32_t *)previousBuffer; - (void)screenSizeChanged; +- (void)setRumble: (double)amp; @end diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index 7e425c5..e5cb7c8 100644 --- a/Cocoa/GBView.m +++ b/Cocoa/GBView.m @@ -1,4 +1,4 @@ -#import +#import #import "GBView.h" #import "GBViewGL.h" #import "GBViewMetal.h" @@ -18,7 +18,11 @@ bool axisActive[2]; bool underclockKeyDown; double clockMultiplier; + double analogClockMultiplier; + bool analogClockMultiplierValid; NSEventModifierFlags previousModifiers; + JOYController *lastController; + GB_frame_blending_mode_t _frameBlendingMode; } + (instancetype)alloc @@ -43,8 +47,7 @@ } - (void) _init -{ - _shouldBlendFrameWithPrevious = 1; +{ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ratioKeepingChanged) name:@"GBAspectChanged" object:nil]; tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){} options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect @@ -55,6 +58,7 @@ [self createInternalView]; [self addSubview:self.internalView]; self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [JOYController registerListener:self]; } - (void)screenSizeChanged @@ -65,9 +69,9 @@ size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(_gb) * GB_get_screen_height(_gb); - image_buffers[0] = malloc(buffer_size); - image_buffers[1] = malloc(buffer_size); - image_buffers[2] = malloc(buffer_size); + image_buffers[0] = calloc(1, buffer_size); + image_buffers[1] = calloc(1, buffer_size); + image_buffers[2] = calloc(1, buffer_size); dispatch_async(dispatch_get_main_queue(), ^{ [self setFrame:self.superview.frame]; @@ -79,15 +83,26 @@ [self setFrame:self.superview.frame]; } -- (void) setShouldBlendFrameWithPrevious:(BOOL)shouldBlendFrameWithPrevious +- (void) setFrameBlendingMode:(GB_frame_blending_mode_t)frameBlendingMode { - _shouldBlendFrameWithPrevious = shouldBlendFrameWithPrevious; + _frameBlendingMode = frameBlendingMode; [self setNeedsDisplay:YES]; } + +- (GB_frame_blending_mode_t)frameBlendingMode +{ + if (_frameBlendingMode == GB_FRAME_BLENDING_MODE_ACCURATE) { + if (!_gb || GB_is_sgb(_gb)) { + return GB_FRAME_BLENDING_MODE_SIMPLE; + } + return GB_is_odd_frame(_gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; + } + return _frameBlendingMode; +} - (unsigned char) numberOfBuffers { - return _shouldBlendFrameWithPrevious? 3 : 2; + return _frameBlendingMode? 3 : 2; } - (void)dealloc @@ -100,11 +115,12 @@ [NSCursor unhide]; } [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self setRumble:0]; + [JOYController unregisterListener:self]; } - (instancetype)initWithCoder:(NSCoder *)coder { - if (!(self = [super initWithCoder:coder])) - { + if (!(self = [super initWithCoder:coder])) { return self; } [self _init]; @@ -113,8 +129,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect { - if (!(self = [super initWithFrame:frameRect])) - { + if (!(self = [super initWithFrame:frameRect])) { return self; } [self _init]; @@ -147,13 +162,21 @@ - (void) flip { - if (underclockKeyDown && clockMultiplier > 0.5) { - clockMultiplier -= 0.1; - GB_set_clock_multiplier(_gb, clockMultiplier); + if (analogClockMultiplierValid && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]) { + GB_set_clock_multiplier(_gb, analogClockMultiplier); + if (analogClockMultiplier == 1.0) { + analogClockMultiplierValid = false; + } } - if (!underclockKeyDown && clockMultiplier < 1.0) { - clockMultiplier += 0.1; - GB_set_clock_multiplier(_gb, clockMultiplier); + else { + if (underclockKeyDown && clockMultiplier > 0.5) { + clockMultiplier -= 1.0/16; + GB_set_clock_multiplier(_gb, clockMultiplier); + } + if (!underclockKeyDown && clockMultiplier < 1.0) { + clockMultiplier += 1.0/16; + GB_set_clock_multiplier(_gb, clockMultiplier); + } } current_buffer = (current_buffer + 1) % self.numberOfBuffers; } @@ -180,6 +203,7 @@ switch (button) { case GBTurbo: GB_set_turbo_mode(_gb, true, self.isRewinding); + analogClockMultiplierValid = false; break; case GBRewind: @@ -189,6 +213,7 @@ case GBUnderclock: underclockKeyDown = true; + analogClockMultiplierValid = false; break; default: @@ -221,6 +246,7 @@ switch (button) { case GBTurbo: GB_set_turbo_mode(_gb, false, false); + analogClockMultiplierValid = false; break; case GBRewind: @@ -229,6 +255,7 @@ case GBUnderclock: underclockKeyDown = false; + analogClockMultiplierValid = false; break; default: @@ -243,105 +270,102 @@ } } -- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state +- (void)setRumble:(double)amp { + [lastController setRumbleAmplitude:amp]; +} + +- (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis +{ + if (![self.window isMainWindow]) return; + + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + if (!mapping) { + mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; + } + + if ((axis.usage == JOYAxisUsageR1 && !mapping) || + axis.uniqueID == [mapping[@"AnalogUnderclock"] unsignedLongValue]){ + analogClockMultiplier = MIN(MAX(1 - axis.value + 0.2, 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); + analogClockMultiplierValid = true; + } +} + +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button +{ + if (![self.window isMainWindow]) return; + if (controller != lastController) { + [self setRumble:0]; + lastController = controller; + } + + unsigned player_count = GB_get_player_count(_gb); - UpdateSystemActivity(UsrActivity); + IOPMAssertionID assertionID; + IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); + for (unsigned player = 0; player < player_count; player++) { - NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"] - objectForKey:[NSString stringWithFormat:@"%u", player]]; + NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"] + objectForKey:n2s(player)]; if (player_count != 1 && // Single player, accpet inputs from all joypads !(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads - ![preferred_joypad isEqualToString:joystick_name]) { + ![preferred_joypad isEqualToString:controller.uniqueID]) { continue; } - NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name]; + dispatch_async(dispatch_get_main_queue(), ^{ + [controller setPlayerLEDs:1 << player]; + }); + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + if (!mapping) { + mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; + } - for (GBButton i = 0; i < GBButtonCount; i++) { - NSNumber *mapped_button = [mapping objectForKey:GBButtonNames[i]]; - if (mapped_button && [mapped_button integerValue] == button) { - switch (i) { - case GBTurbo: - GB_set_turbo_mode(_gb, state, state && self.isRewinding); - break; - - case GBRewind: - self.isRewinding = state; - if (state) { - GB_set_turbo_mode(_gb, false, false); - } - break; - - case GBUnderclock: - underclockKeyDown = state; - break; - - default: - GB_set_key_state_for_player(_gb, (GB_key_t)i, player, state); - break; + JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: button.usage; + if (!mapping && usage >= JOYButtonUsageGeneric0) { + usage = (const JOYButtonUsage[]){JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX}[(usage - JOYButtonUsageGeneric0) & 3]; + } + + 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 JOYButtonUsageC: break; + case JOYButtonUsageStart: + case JOYButtonUsageX: GB_set_key_state_for_player(_gb, GB_KEY_START, player, button.isPressed); break; + case JOYButtonUsageSelect: + case JOYButtonUsageY: GB_set_key_state_for_player(_gb, GB_KEY_SELECT, player, button.isPressed); break; + case JOYButtonUsageR2: + case JOYButtonUsageL2: + case JOYButtonUsageZ: { + self.isRewinding = button.isPressed; + if (button.isPressed) { + GB_set_turbo_mode(_gb, false, false); } + break; } + + case JOYButtonUsageL1: GB_set_turbo_mode(_gb, button.isPressed, button.isPressed && self.isRewinding); 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; + + default: + break; } } } -- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value -{ - unsigned player_count = GB_get_player_count(_gb); - - UpdateSystemActivity(UsrActivity); - for (unsigned player = 0; player < player_count; player++) { - NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"] - objectForKey:[NSString stringWithFormat:@"%u", player]]; - if (player_count != 1 && // Single player, accpet inputs from all joypads - !(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads - ![preferred_joypad isEqualToString:joystick_name]) { - continue; - } - - NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name]; - NSNumber *x_axis = [mapping objectForKey:@"XAxis"]; - NSNumber *y_axis = [mapping objectForKey:@"YAxis"]; - - if (axis == [x_axis integerValue]) { - if (value > JOYSTICK_HIGH) { - axisActive[0] = true; - GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, true); - GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, false); - } - else if (value < -JOYSTICK_HIGH) { - axisActive[0] = true; - GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, false); - GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, true); - } - else if (axisActive[0] && value < JOYSTICK_LOW && value > -JOYSTICK_LOW) { - axisActive[0] = false; - GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, false); - GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, false); - } - } - else if (axis == [y_axis integerValue]) { - if (value > JOYSTICK_HIGH) { - axisActive[1] = true; - GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, true); - GB_set_key_state_for_player(_gb, GB_KEY_UP, player, false); - } - else if (value < -JOYSTICK_HIGH) { - axisActive[1] = true; - GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, false); - GB_set_key_state_for_player(_gb, GB_KEY_UP, player, true); - } - else if (axisActive[1] && value < JOYSTICK_LOW && value > -JOYSTICK_LOW) { - axisActive[1] = false; - GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, false); - GB_set_key_state_for_player(_gb, GB_KEY_UP, player, false); - } - } - } -} - - - (BOOL)acceptsFirstResponder { return YES; diff --git a/Cocoa/GBViewMetal.m b/Cocoa/GBViewMetal.m index 9acb11e..9a1c78b 100644 --- a/Cocoa/GBViewMetal.m +++ b/Cocoa/GBViewMetal.m @@ -1,4 +1,6 @@ #import "GBViewMetal.h" +#pragma clang diagnostic ignored "-Wpartial-availability" + static const vector_float2 rect[] = { @@ -15,7 +17,7 @@ static const vector_float2 rect[] = id vertices; id pipeline_state; id command_queue; - id mix_previous_buffer; + id frame_blending_mode_buffer; id output_resolution_buffer; vector_float2 output_resolution; } @@ -50,15 +52,16 @@ static const vector_float2 rect[] = view.delegate = self; self.internalView = view; view.paused = YES; + view.enableSetNeedsDisplay = YES; vertices = [device newBufferWithBytes:rect length:sizeof(rect) options:MTLResourceStorageModeShared]; - static const bool default_mix_value = false; - mix_previous_buffer = [device newBufferWithBytes:&default_mix_value - length:sizeof(default_mix_value) - options:MTLResourceStorageModeShared]; + static const GB_frame_blending_mode_t default_blending_mode = GB_FRAME_BLENDING_MODE_DISABLED; + frame_blending_mode_buffer = [device newBufferWithBytes:&default_blending_mode + length:sizeof(default_blending_mode) + options:MTLResourceStorageModeShared]; output_resolution_buffer = [device newBufferWithBytes:&output_resolution length:sizeof(output_resolution) @@ -131,6 +134,7 @@ static const vector_float2 rect[] = - (void)drawInMTKView:(nonnull MTKView *)view { if (!(view.window.occlusionState & NSWindowOcclusionStateVisible)) return; + if (!self.gb) return; if (texture.width != GB_get_screen_width(self.gb) || texture.height != GB_get_screen_height(self.gb)) { [self allocateTextures]; @@ -145,7 +149,7 @@ static const vector_float2 rect[] = mipmapLevel:0 withBytes:[self currentBuffer] bytesPerRow:texture.width * 4]; - if ([self shouldBlendFrameWithPrevious]) { + if ([self frameBlendingMode]) { [previous_texture replaceRegion:region mipmapLevel:0 withBytes:[self previousBuffer] @@ -155,9 +159,8 @@ static const vector_float2 rect[] = MTLRenderPassDescriptor *render_pass_descriptor = view.currentRenderPassDescriptor; id command_buffer = [command_queue commandBuffer]; - if(render_pass_descriptor != nil) - { - *(bool *)[mix_previous_buffer contents] = [self shouldBlendFrameWithPrevious]; + if (render_pass_descriptor != nil) { + *(GB_frame_blending_mode_t *)[frame_blending_mode_buffer contents] = [self frameBlendingMode]; *(vector_float2 *)[output_resolution_buffer contents] = output_resolution; id render_encoder = @@ -174,7 +177,7 @@ static const vector_float2 rect[] = offset:0 atIndex:0]; - [render_encoder setFragmentBuffer:mix_previous_buffer + [render_encoder setFragmentBuffer:frame_blending_mode_buffer offset:0 atIndex:0]; @@ -205,7 +208,7 @@ static const vector_float2 rect[] = { [super flip]; dispatch_async(dispatch_get_main_queue(), ^{ - [(MTKView *)self.internalView draw]; + [(MTKView *)self.internalView setNeedsDisplay:YES]; }); } diff --git a/Cocoa/Info.plist b/Cocoa/Info.plist index 65f0df8..44a21f0 100644 --- a/Cocoa/Info.plist +++ b/Cocoa/Info.plist @@ -2,6 +2,8 @@ + CFBundleDisplayName + SameBoy CFBundleDevelopmentRegion en CFBundleDocumentTypes @@ -14,7 +16,7 @@ CFBundleTypeIconFile Cartridge CFBundleTypeName - GameBoy Game + Game Boy Game CFBundleTypeRole Viewer LSItemContentTypes @@ -34,7 +36,7 @@ CFBundleTypeIconFile ColorCartridge CFBundleTypeName - GameBoy Color Game + Game Boy Color Game CFBundleTypeRole Viewer LSItemContentTypes @@ -46,6 +48,26 @@ NSDocumentClass Document + + CFBundleTypeExtensions + + gbc + + CFBundleTypeIconFile + ColorCartridge + CFBundleTypeName + Game Boy ISX File + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.github.liji32.sameboy.isx + + LSTypeIsPackage + 0 + NSDocumentClass + Document + CFBundleExecutable SameBoy @@ -70,7 +92,7 @@ LSMinimumSystemVersion 10.9 NSHumanReadableCopyright - Copyright © 2015-2018 Lior Halphon + Copyright © 2015-2020 Lior Halphon NSMainNibFile MainMenu NSPrincipalClass @@ -83,7 +105,7 @@ public.data UTTypeDescription - GameBoy Game + Game Boy Game UTTypeIconFile Cartridge UTTypeIdentifier @@ -102,7 +124,7 @@ public.data UTTypeDescription - GameBoy Color Game + Game Boy Color Game UTTypeIconFile ColorCartridge UTTypeIdentifier @@ -115,7 +137,28 @@ + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy ISX File + UTTypeIconFile + ColorCartridge + UTTypeIdentifier + com.github.liji32.sameboy.isx + UTTypeTagSpecification + + public.filename-extension + + isx + + + + NSCameraUsageDescription + SameBoy needs to access your camera to emulate the Game Boy Camera NSSupportsAutomaticGraphicsSwitching diff --git a/Cocoa/Joypad~dark.png b/Cocoa/Joypad~dark.png new file mode 100644 index 0000000..8a7687b Binary files /dev/null and b/Cocoa/Joypad~dark.png differ diff --git a/Cocoa/Joypad~dark@2x.png b/Cocoa/Joypad~dark@2x.png new file mode 100644 index 0000000..ce2a07c Binary files /dev/null and b/Cocoa/Joypad~dark@2x.png differ diff --git a/Cocoa/License.html b/Cocoa/License.html index 2673143..b21cf8d 100644 --- a/Cocoa/License.html +++ b/Cocoa/License.html @@ -30,7 +30,7 @@

SameBoy

MIT License

-

Copyright © 2015-2018 Lior Halphon

+

Copyright © 2015-2020 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 844aa0c..71add1c 100644 --- a/Cocoa/MainMenu.xib +++ b/Cocoa/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -342,11 +342,22 @@ - - + + + + + +

+ + + + + + + - + @@ -371,9 +382,9 @@ - + - + @@ -454,6 +465,7 @@ +
diff --git a/Cocoa/NSImageNamedDarkSupport.m b/Cocoa/NSImageNamedDarkSupport.m new file mode 100644 index 0000000..821ba3b --- /dev/null +++ b/Cocoa/NSImageNamedDarkSupport.m @@ -0,0 +1,42 @@ +#import +#import + +@interface NSImageRep(PrivateAPI) +@property(setter=_setAppearanceName:) NSString *_appearanceName; +@end + +static NSImage * (*imageNamed)(Class self, SEL _cmd, NSString *name); + +@implementation NSImage(DarkHooks) + ++ (NSImage *)imageNamedWithDark:(NSImageName)name +{ + NSImage *light = imageNamed(self, _cmd, name); + if (@available(macOS 10.14, *)) { + NSImage *dark = imageNamed(self, _cmd, [name stringByAppendingString:@"~dark"]); + if (!dark) { + return light; + } + NSImage *ret = [[NSImage alloc] initWithSize:light.size]; + for (NSImageRep *rep in light.representations) { + [rep _setAppearanceName:NSAppearanceNameAqua]; + [ret addRepresentation:rep]; + } + for (NSImageRep *rep in dark.representations) { + [rep _setAppearanceName:NSAppearanceNameDarkAqua]; + [ret addRepresentation:rep]; + } + return ret; + } + return light; +} + ++(void)load +{ + if (@available(macOS 10.14, *)) { + imageNamed = (void *)[self methodForSelector:@selector(imageNamed:)]; + method_setImplementation(class_getClassMethod(self, @selector(imageNamed:)), + [self methodForSelector:@selector(imageNamedWithDark:)]); + } +} +@end diff --git a/Cocoa/NSObject+MavericksCompat.m b/Cocoa/NSObject+MavericksCompat.m new file mode 100644 index 0000000..6c06514 --- /dev/null +++ b/Cocoa/NSObject+MavericksCompat.m @@ -0,0 +1,7 @@ +#import +@implementation NSObject (MavericksCompat) +- (instancetype)initWithCoder:(NSCoder *)coder +{ + return [self init]; +} +@end diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index e754199..99c6543 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -1,8 +1,8 @@ - + - + @@ -17,7 +17,7 @@ - + @@ -58,53 +58,59 @@ + + + + + - + - + - + - + - + - + - - - + + + + @@ -127,16 +133,16 @@ - + - + - + @@ -146,9 +152,10 @@ - - - + + + + @@ -156,10 +163,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -201,7 +296,7 @@ - + @@ -219,12 +314,12 @@ - + - + @@ -243,7 +338,7 @@ - + @@ -271,7 +366,7 @@ - + @@ -288,7 +383,7 @@ - + @@ -301,7 +396,7 @@ - + @@ -339,16 +434,16 @@ - + - + - - + + @@ -359,7 +454,7 @@ - + @@ -369,22 +464,11 @@ - + - - + @@ -392,14 +476,23 @@ + + + + + + + + + - + - + @@ -414,8 +507,8 @@ - - + + @@ -427,8 +520,8 @@ - - + + @@ -441,28 +534,28 @@ - - + - + - + - + @@ -476,11 +569,11 @@ - + - + @@ -489,7 +582,7 @@ - + @@ -507,10 +600,39 @@ - + + - + diff --git a/Cocoa/Speaker~dark.png b/Cocoa/Speaker~dark.png new file mode 100644 index 0000000..f3f820a Binary files /dev/null and b/Cocoa/Speaker~dark.png differ diff --git a/Cocoa/Speaker~dark@2x.png b/Cocoa/Speaker~dark@2x.png new file mode 100644 index 0000000..bdc3eb7 Binary files /dev/null and b/Cocoa/Speaker~dark@2x.png differ diff --git a/Cocoa/joypad.m b/Cocoa/joypad.m deleted file mode 100755 index bca4097..0000000 --- a/Cocoa/joypad.m +++ /dev/null @@ -1,684 +0,0 @@ -/* - Joypad support is based on a stripped-down version of SDL's Darwin implementation - of the Joystick API, under the following license: -*/ - -/* - Simple DirectMedia Layer - Copyright (C) 1997-2017 Sam Lantinga - - This software is provided 'as-is', without any express or implied - warranty. In no event will the authors be held liable for any damages - arising from the use of this software. - - Permission is granted to anyone to use this software for any purpose, - including commercial applications, and to alter it and redistribute it - freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - 3. This notice may not be removed or altered from any source distribution. -*/ - -#include -#include -#include -#include -#include "GBJoystickListener.h" - -typedef signed SDL_JoystickID; -typedef struct _SDL_Joystick SDL_Joystick; - -typedef struct _SDL_JoystickAxisInfo -{ - int16_t initial_value; /* Initial axis state */ - int16_t value; /* Current axis state */ - int16_t zero; /* Zero point on the axis (-32768 for triggers) */ - bool has_initial_value; /* Whether we've seen a value on the axis yet */ - bool sent_initial_value; /* Whether we've sent the initial axis value */ -} SDL_JoystickAxisInfo; - -struct _SDL_Joystick -{ - SDL_JoystickID instance_id; /* Device instance, monotonically increasing from 0 */ - char *name; /* Joystick name - system dependent */ - - int naxes; /* Number of axis controls on the joystick */ - SDL_JoystickAxisInfo *axes; - - int nbuttons; /* Number of buttons on the joystick */ - uint8_t *buttons; /* Current button states */ - - struct joystick_hwdata *hwdata; /* Driver dependent information */ - - int ref_count; /* Reference count for multiple opens */ - - bool is_game_controller; - bool force_recentering; /* SDL_TRUE if this device needs to have its state reset to 0 */ - struct _SDL_Joystick *next; /* pointer to next joystick we have allocated */ -}; - -typedef struct { - uint8_t data[16]; -} SDL_JoystickGUID; - -struct recElement -{ - IOHIDElementRef elementRef; - IOHIDElementCookie cookie; - uint32_t usagePage, usage; /* HID usage */ - SInt32 min; /* reported min value possible */ - SInt32 max; /* reported max value possible */ - - /* runtime variables used for auto-calibration */ - SInt32 minReport; /* min returned value */ - SInt32 maxReport; /* max returned value */ - - struct recElement *pNext; /* next element in list */ -}; -typedef struct recElement recElement; - -struct joystick_hwdata -{ - IOHIDDeviceRef deviceRef; /* HIDManager device handle */ - io_service_t ffservice; /* Interface for force feedback, 0 = no ff */ - - char product[256]; /* name of product */ - uint32_t usage; /* usage page from IOUSBHID Parser.h which defines general usage */ - uint32_t usagePage; /* usage within above page from IOUSBHID Parser.h which defines specific usage */ - - int axes; /* number of axis (calculated, not reported by device) */ - int buttons; /* number of buttons (calculated, not reported by device) */ - int elements; /* number of total elements (should be total of above) (calculated, not reported by device) */ - - recElement *firstAxis; - recElement *firstButton; - - bool removed; - - int instance_id; - SDL_JoystickGUID guid; - - SDL_Joystick joystick; -}; -typedef struct joystick_hwdata recDevice; - -/* The base object of the HID Manager API */ -static IOHIDManagerRef hidman = NULL; - -/* static incrementing counter for new joystick devices seen on the system. Devices should start with index 0 */ -static int s_joystick_instance_id = -1; - -#define SDL_JOYSTICK_AXIS_MAX 32767 - -void SDL_PrivateJoystickAxis(SDL_Joystick * joystick, uint8_t axis, int16_t value) -{ - /* Make sure we're not getting garbage or duplicate events */ - if (axis >= joystick->naxes) { - return; - } - if (!joystick->axes[axis].has_initial_value) { - joystick->axes[axis].initial_value = value; - joystick->axes[axis].value = value; - joystick->axes[axis].zero = value; - joystick->axes[axis].has_initial_value = true; - } - if (value == joystick->axes[axis].value) { - return; - } - if (!joystick->axes[axis].sent_initial_value) { - /* Make sure we don't send motion until there's real activity on this axis */ - const int MAX_ALLOWED_JITTER = SDL_JOYSTICK_AXIS_MAX / 80; /* ShanWan PS3 controller needed 96 */ - if (abs(value - joystick->axes[axis].value) <= MAX_ALLOWED_JITTER) { - return; - } - joystick->axes[axis].sent_initial_value = true; - joystick->axes[axis].value = value; /* Just so we pass the check above */ - SDL_PrivateJoystickAxis(joystick, axis, joystick->axes[axis].initial_value); - } - - /* Update internal joystick state */ - joystick->axes[axis].value = value; - - NSResponder *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder]; - while (responder) { - if ([responder respondsToSelector:@selector(joystick:axis:movedTo:)]) { - [responder joystick:@(joystick->name) axis:axis movedTo:value]; - break; - } - responder = (typeof(responder)) [responder nextResponder]; - } -} - -void SDL_PrivateJoystickButton(SDL_Joystick *joystick, uint8_t button, uint8_t state) -{ - - /* Make sure we're not getting garbage or duplicate events */ - if (button >= joystick->nbuttons) { - return; - } - if (state == joystick->buttons[button]) { - return; - } - - /* Update internal joystick state */ - joystick->buttons[button] = state; - - NSResponder *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder]; - while (responder) { - if ([responder respondsToSelector:@selector(joystick:button:changedState:)]) { - [responder joystick:@(joystick->name) button:button changedState:state]; - break; - } - responder = (typeof(responder)) [responder nextResponder]; - } -} - -static void -FreeElementList(recElement *pElement) -{ - while (pElement) { - recElement *pElementNext = pElement->pNext; - free(pElement); - pElement = pElementNext; - } -} - - -static recDevice * -FreeDevice(recDevice *removeDevice) -{ - recDevice *pDeviceNext = NULL; - if (removeDevice) { - if (removeDevice->deviceRef) { - IOHIDDeviceUnscheduleFromRunLoop(removeDevice->deviceRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - removeDevice->deviceRef = NULL; - } - - /* free element lists */ - FreeElementList(removeDevice->firstAxis); - FreeElementList(removeDevice->firstButton); - - free(removeDevice); - } - return pDeviceNext; -} - -static SInt32 -GetHIDElementState(recDevice *pDevice, recElement *pElement) -{ - SInt32 value = 0; - - if (pDevice && pElement) { - IOHIDValueRef valueRef; - if (IOHIDDeviceGetValue(pDevice->deviceRef, pElement->elementRef, &valueRef) == kIOReturnSuccess) { - value = (SInt32) IOHIDValueGetIntegerValue(valueRef); - - /* record min and max for auto calibration */ - if (value < pElement->minReport) { - pElement->minReport = value; - } - if (value > pElement->maxReport) { - pElement->maxReport = value; - } - } - } - - return value; -} - -static SInt32 -GetHIDScaledCalibratedState(recDevice * pDevice, recElement * pElement, SInt32 min, SInt32 max) -{ - const float deviceScale = max - min; - const float readScale = pElement->maxReport - pElement->minReport; - const SInt32 value = GetHIDElementState(pDevice, pElement); - if (readScale == 0) { - return value; /* no scaling at all */ - } - return ((value - pElement->minReport) * deviceScale / readScale) + min; -} - - -static void -JoystickDeviceWasRemovedCallback(void *ctx, IOReturn result, void *sender) -{ - recDevice *device = (recDevice *) ctx; - device->removed = true; - device->deviceRef = NULL; // deviceRef was invalidated due to the remove - FreeDevice(device); -} - - -static void AddHIDElement(const void *value, void *parameter); - -/* Call AddHIDElement() on all elements in an array of IOHIDElementRefs */ -static void -AddHIDElements(CFArrayRef array, recDevice *pDevice) -{ - const CFRange range = { 0, CFArrayGetCount(array) }; - CFArrayApplyFunction(array, range, AddHIDElement, pDevice); -} - -static bool -ElementAlreadyAdded(const IOHIDElementCookie cookie, const recElement *listitem) { - while (listitem) { - if (listitem->cookie == cookie) { - return true; - } - listitem = listitem->pNext; - } - return false; -} - -/* See if we care about this HID element, and if so, note it in our recDevice. */ -static void -AddHIDElement(const void *value, void *parameter) -{ - recDevice *pDevice = (recDevice *) parameter; - IOHIDElementRef refElement = (IOHIDElementRef) value; - const CFTypeID elementTypeID = refElement ? CFGetTypeID(refElement) : 0; - - if (refElement && (elementTypeID == IOHIDElementGetTypeID())) { - const IOHIDElementCookie cookie = IOHIDElementGetCookie(refElement); - const uint32_t usagePage = IOHIDElementGetUsagePage(refElement); - const uint32_t usage = IOHIDElementGetUsage(refElement); - recElement *element = NULL; - recElement **headElement = NULL; - - /* look at types of interest */ - switch (IOHIDElementGetType(refElement)) { - case kIOHIDElementTypeInput_Misc: - case kIOHIDElementTypeInput_Button: - case kIOHIDElementTypeInput_Axis: { - switch (usagePage) { /* only interested in kHIDPage_GenericDesktop and kHIDPage_Button */ - case kHIDPage_GenericDesktop: - switch (usage) { - case kHIDUsage_GD_X: - case kHIDUsage_GD_Y: - case kHIDUsage_GD_Z: - case kHIDUsage_GD_Rx: - case kHIDUsage_GD_Ry: - case kHIDUsage_GD_Rz: - case kHIDUsage_GD_Slider: - case kHIDUsage_GD_Dial: - case kHIDUsage_GD_Wheel: - if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) { - element = (recElement *) calloc(1, sizeof (recElement)); - if (element) { - pDevice->axes++; - headElement = &(pDevice->firstAxis); - } - } - break; - - case kHIDUsage_GD_DPadUp: - case kHIDUsage_GD_DPadDown: - case kHIDUsage_GD_DPadRight: - case kHIDUsage_GD_DPadLeft: - case kHIDUsage_GD_Start: - case kHIDUsage_GD_Select: - case kHIDUsage_GD_SystemMainMenu: - if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) { - element = (recElement *) calloc(1, sizeof (recElement)); - if (element) { - pDevice->buttons++; - headElement = &(pDevice->firstButton); - } - } - break; - } - break; - - case kHIDPage_Simulation: - switch (usage) { - case kHIDUsage_Sim_Rudder: - case kHIDUsage_Sim_Throttle: - case kHIDUsage_Sim_Accelerator: - case kHIDUsage_Sim_Brake: - if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) { - element = (recElement *) calloc(1, sizeof (recElement)); - if (element) { - pDevice->axes++; - headElement = &(pDevice->firstAxis); - } - } - break; - - default: - break; - } - break; - - case kHIDPage_Button: - case kHIDPage_Consumer: /* e.g. 'pause' button on Steelseries MFi gamepads. */ - if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) { - element = (recElement *) calloc(1, sizeof (recElement)); - if (element) { - pDevice->buttons++; - headElement = &(pDevice->firstButton); - } - } - break; - - default: - break; - } - } - break; - - case kIOHIDElementTypeCollection: { - CFArrayRef array = IOHIDElementGetChildren(refElement); - if (array) { - AddHIDElements(array, pDevice); - } - } - break; - - default: - break; - } - - if (element && headElement) { /* add to list */ - recElement *elementPrevious = NULL; - recElement *elementCurrent = *headElement; - while (elementCurrent && usage >= elementCurrent->usage) { - elementPrevious = elementCurrent; - elementCurrent = elementCurrent->pNext; - } - if (elementPrevious) { - elementPrevious->pNext = element; - } else { - *headElement = element; - } - - element->elementRef = refElement; - element->usagePage = usagePage; - element->usage = usage; - element->pNext = elementCurrent; - - element->minReport = element->min = (SInt32) IOHIDElementGetLogicalMin(refElement); - element->maxReport = element->max = (SInt32) IOHIDElementGetLogicalMax(refElement); - element->cookie = IOHIDElementGetCookie(refElement); - - pDevice->elements++; - } - } -} - -static bool -GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice) -{ - const uint16_t BUS_USB = 0x03; - const uint16_t BUS_BLUETOOTH = 0x05; - int32_t vendor = 0; - int32_t product = 0; - int32_t version = 0; - CFTypeRef refCF = NULL; - CFArrayRef array = NULL; - uint16_t *guid16 = (uint16_t *)pDevice->guid.data; - - /* get usage page and usage */ - refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsagePageKey)); - if (refCF) { - CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usagePage); - } - if (pDevice->usagePage != kHIDPage_GenericDesktop) { - return false; /* Filter device list to non-keyboard/mouse stuff */ - } - - refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsageKey)); - if (refCF) { - CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usage); - } - - if ((pDevice->usage != kHIDUsage_GD_Joystick && - pDevice->usage != kHIDUsage_GD_GamePad && - pDevice->usage != kHIDUsage_GD_MultiAxisController)) { - return false; /* Filter device list to non-keyboard/mouse stuff */ - } - - pDevice->deviceRef = hidDevice; - - /* get device name */ - refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey)); - if (!refCF) { - /* Maybe we can't get "AwesomeJoystick2000", but we can get "Logitech"? */ - refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDManufacturerKey)); - } - if ((!refCF) || (!CFStringGetCString(refCF, pDevice->product, sizeof (pDevice->product), kCFStringEncodingUTF8))) { - strlcpy(pDevice->product, "Unidentified joystick", sizeof (pDevice->product)); - } - - refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey)); - if (refCF) { - CFNumberGetValue(refCF, kCFNumberSInt32Type, &vendor); - } - - refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey)); - if (refCF) { - CFNumberGetValue(refCF, kCFNumberSInt32Type, &product); - } - - refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVersionNumberKey)); - if (refCF) { - CFNumberGetValue(refCF, kCFNumberSInt32Type, &version); - } - - memset(pDevice->guid.data, 0, sizeof(pDevice->guid.data)); - - if (vendor && product) { - *guid16++ = BUS_USB; - *guid16++ = 0; - *guid16++ = vendor; - *guid16++ = 0; - *guid16++ = product; - *guid16++ = 0; - *guid16++ = version; - *guid16++ = 0; - } else { - *guid16++ = BUS_BLUETOOTH; - *guid16++ = 0; - strlcpy((char*)guid16, pDevice->product, sizeof(pDevice->guid.data) - 4); - } - - array = IOHIDDeviceCopyMatchingElements(hidDevice, NULL, kIOHIDOptionsTypeNone); - if (array) { - AddHIDElements(array, pDevice); - CFRelease(array); - } - - return true; -} - -void -SDL_SYS_JoystickUpdate(SDL_Joystick * joystick) -{ - recDevice *device = joystick->hwdata; - recElement *element; - SInt32 value; - int i; - - if (!device) { - return; - } - - if (device->removed) { /* device was unplugged; ignore it. */ - if (joystick->hwdata) { - joystick->force_recentering = true; - joystick->hwdata = NULL; - } - return; - } - - element = device->firstAxis; - i = 0; - while (element) { - value = GetHIDScaledCalibratedState(device, element, -32768, 32767); - SDL_PrivateJoystickAxis(joystick, i, value); - element = element->pNext; - ++i; - } - - element = device->firstButton; - i = 0; - while (element) { - value = GetHIDElementState(device, element); - if (value > 1) { /* handle pressure-sensitive buttons */ - value = 1; - } - SDL_PrivateJoystickButton(joystick, i, value); - element = element->pNext; - ++i; - } -} - -static void JoystickInputCallback( - SDL_Joystick * joystick, - IOReturn result, - void * _Nullable sender, - IOHIDReportType type, - uint32_t reportID, - uint8_t * report, - CFIndex reportLength) -{ - SDL_SYS_JoystickUpdate(joystick); -} - -static void -JoystickDeviceWasAddedCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject) -{ - recDevice *device; - io_service_t ioservice; - - if (res != kIOReturnSuccess) { - return; - } - - device = (recDevice *) calloc(1, sizeof(recDevice)); - - if (!device) { - abort(); - return; - } - - if (!GetDeviceInfo(ioHIDDeviceObject, device)) { - free(device); - return; /* not a device we care about, probably. */ - } - - SDL_Joystick *joystick = &device->joystick; - - joystick->instance_id = device->instance_id; - joystick->hwdata = device; - joystick->name = device->product; - - joystick->naxes = device->axes; - joystick->nbuttons = device->buttons; - - if (joystick->naxes > 0) { - joystick->axes = (SDL_JoystickAxisInfo *) calloc(joystick->naxes, sizeof(SDL_JoystickAxisInfo)); - } - if (joystick->nbuttons > 0) { - joystick->buttons = (uint8_t *) calloc(joystick->nbuttons, 1); - } - - /* Get notified when this device is disconnected. */ - IOHIDDeviceRegisterRemovalCallback(ioHIDDeviceObject, JoystickDeviceWasRemovedCallback, device); - static uint8_t junk[80]; - IOHIDDeviceRegisterInputReportCallback(ioHIDDeviceObject, junk, sizeof(junk), (IOHIDReportCallback) JoystickInputCallback, joystick); - IOHIDDeviceScheduleWithRunLoop(ioHIDDeviceObject, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - - /* Allocate an instance ID for this device */ - device->instance_id = ++s_joystick_instance_id; - - /* We have to do some storage of the io_service_t for SDL_HapticOpenFromJoystick */ - ioservice = IOHIDDeviceGetService(ioHIDDeviceObject); -} - -static bool -ConfigHIDManager(CFArrayRef matchingArray) -{ - CFRunLoopRef runloop = CFRunLoopGetCurrent(); - - if (IOHIDManagerOpen(hidman, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { - return false; - } - - IOHIDManagerSetDeviceMatchingMultiple(hidman, matchingArray); - IOHIDManagerRegisterDeviceMatchingCallback(hidman, JoystickDeviceWasAddedCallback, NULL); - IOHIDManagerScheduleWithRunLoop(hidman, runloop, kCFRunLoopDefaultMode); - - return true; /* good to go. */ -} - - -static CFDictionaryRef -CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage, int *okay) -{ - CFDictionaryRef retval = NULL; - CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page); - CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); - const void *keys[2] = { (void *) CFSTR(kIOHIDDeviceUsagePageKey), (void *) CFSTR(kIOHIDDeviceUsageKey) }; - const void *vals[2] = { (void *) pageNumRef, (void *) usageNumRef }; - - if (pageNumRef && usageNumRef) { - retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); - } - - if (pageNumRef) { - CFRelease(pageNumRef); - } - if (usageNumRef) { - CFRelease(usageNumRef); - } - - if (!retval) { - *okay = 0; - } - - return retval; -} - -static bool -CreateHIDManager(void) -{ - bool retval = false; - int okay = 1; - const void *vals[] = { - (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay), - (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay), - (void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay), - }; - const size_t numElements = sizeof(vals) / sizeof(vals[0]); - CFArrayRef array = okay ? CFArrayCreate(kCFAllocatorDefault, vals, numElements, &kCFTypeArrayCallBacks) : NULL; - size_t i; - - for (i = 0; i < numElements; i++) { - if (vals[i]) { - CFRelease((CFTypeRef) vals[i]); - } - } - - if (array) { - hidman = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); - if (hidman != NULL) { - retval = ConfigHIDManager(array); - } - CFRelease(array); - } - - return retval; -} - - -void __attribute__((constructor)) SDL_SYS_JoystickInit(void) -{ - if (!CreateHIDManager()) { - fprintf(stderr, "Joystick: Couldn't initialize HID Manager"); - } -} diff --git a/Cocoa/main.m b/Cocoa/main.m index 8a6799b..3eb7a37 100644 --- a/Cocoa/main.m +++ b/Cocoa/main.m @@ -1,5 +1,6 @@ #import -int main(int argc, const char * argv[]) { +int main(int argc, const char * argv[]) +{ return NSApplicationMain(argc, argv); } diff --git a/Core/apu.c b/Core/apu.c index 1b3dd2b..7e7ab31 100644 --- a/Core/apu.c +++ b/Core/apu.c @@ -1,6 +1,7 @@ #include #include #include +#include #include "gb.h" #define likely(x) __builtin_expect((x), 1) @@ -21,34 +22,99 @@ static void refresh_channel(GB_gameboy_t *gb, unsigned index, unsigned cycles_of gb->apu_output.last_update[index] = gb->apu_output.cycles_since_render + cycles_offset; } -static bool is_DAC_enabled(GB_gameboy_t *gb, unsigned index) +bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index) { + if (gb->model >= GB_MODEL_AGB) { + /* On the AGB, mixing is done digitally, so there are no per-channel + DACs. Instead, all channels are summed digital regardless of + whatever the DAC state would be on a CGB or earlier model. */ + return true; + } + switch (index) { case GB_SQUARE_1: return gb->io_registers[GB_IO_NR12] & 0xF8; - + case GB_SQUARE_2: return gb->io_registers[GB_IO_NR22] & 0xF8; - + case GB_WAVE: return gb->apu.wave_channel.enable; - + case GB_NOISE: return gb->io_registers[GB_IO_NR42] & 0xF8; } + + return false; +} + +static uint8_t agb_bias_for_channel(GB_gameboy_t *gb, unsigned index) +{ + if (!gb->apu.is_active[index]) return 0; + switch (index) { + case GB_SQUARE_1: + return gb->apu.square_channels[GB_SQUARE_1].current_volume; + case GB_SQUARE_2: + return gb->apu.square_channels[GB_SQUARE_2].current_volume; + case GB_WAVE: + return 0; + case GB_NOISE: + return gb->apu.noise_channel.current_volume; + } return 0; } static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsigned cycles_offset) { - if (!is_DAC_enabled(gb, index)) { + 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 + playing PCM sample 0. */ + gb->apu.samples[index] = value; + + if (gb->apu_output.sample_rate) { + 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); + + if (gb->io_registers[GB_IO_NR51] & (1 << index)) { + output.right = (0xf - value * 2 + bias) * right_volume; + } + else { + output.right = 0xf * right_volume; + } + + if (gb->io_registers[GB_IO_NR51] & (0x10 << index)) { + output.left = (0xf - value * 2 + bias) * left_volume; + } + else { + output.left = 0xf * left_volume; + } + + if (*(uint32_t *)&(gb->apu_output.current_sample[index]) != *(uint32_t *)&output) { + refresh_channel(gb, index, cycles_offset); + gb->apu_output.current_sample[index] = output; + } + } + + return; + } + + if (!GB_apu_is_DAC_enabled(gb, index)) { value = gb->apu.samples[index]; } else { gb->apu.samples[index] = value; } - + if (gb->apu_output.sample_rate) { unsigned right_volume = 0; if (gb->io_registers[GB_IO_NR51] & (1 << index)) { @@ -66,33 +132,42 @@ static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsign } } -static void render(GB_gameboy_t *gb, bool no_downsampling, GB_sample_t *dest) +static double smooth(double x) { - GB_sample_t output = {0,0}; - #pragma unroll - for (unsigned i = GB_N_CHANNELS; i--;) { + return 3*x*x - 2*x*x*x; +} + +static void render(GB_gameboy_t *gb) +{ + GB_sample_t output = {0, 0}; + + UNROLL + for (unsigned i = 0; i < GB_N_CHANNELS; i++) { double multiplier = CH_STEP; - if (!is_DAC_enabled(gb, i)) { - gb->apu_output.dac_discharge[i] -= ((double) DAC_DECAY_SPEED) / gb->apu_output.sample_rate; - if (gb->apu_output.dac_discharge[i] < 0) { - multiplier = 0; - gb->apu_output.dac_discharge[i] = 0; + + if (gb->model < GB_MODEL_AGB) { + if (!GB_apu_is_DAC_enabled(gb, i)) { + gb->apu_output.dac_discharge[i] -= ((double) DAC_DECAY_SPEED) / gb->apu_output.sample_rate; + if (gb->apu_output.dac_discharge[i] < 0) { + multiplier = 0; + gb->apu_output.dac_discharge[i] = 0; + } + else { + multiplier *= smooth(gb->apu_output.dac_discharge[i]); + } } else { - multiplier *= gb->apu_output.dac_discharge[i]; - } - } - else { - gb->apu_output.dac_discharge[i] += ((double) DAC_ATTACK_SPEED) / gb->apu_output.sample_rate; - if (gb->apu_output.dac_discharge[i] > 1) { - gb->apu_output.dac_discharge[i] = 1; - } - else { - multiplier *= gb->apu_output.dac_discharge[i]; + gb->apu_output.dac_discharge[i] += ((double) DAC_ATTACK_SPEED) / gb->apu_output.sample_rate; + if (gb->apu_output.dac_discharge[i] > 1) { + gb->apu_output.dac_discharge[i] = 1; + } + else { + multiplier *= smooth(gb->apu_output.dac_discharge[i]); + } } } - if (likely(gb->apu_output.last_update[i] == 0 || no_downsampling)) { + if (likely(gb->apu_output.last_update[i] == 0)) { output.left += gb->apu_output.current_sample[i].left * multiplier; output.right += gb->apu_output.current_sample[i].right * multiplier; } @@ -112,7 +187,7 @@ static void render(GB_gameboy_t *gb, bool no_downsampling, GB_sample_t *dest) (GB_sample_t) {output.left - gb->apu_output.highpass_diff.left, output.right - gb->apu_output.highpass_diff.right} : output; - + switch (gb->apu_output.highpass_mode) { case GB_HIGHPASS_OFF: gb->apu_output.highpass_diff = (GB_double_sample_t) {0, 0}; @@ -126,7 +201,7 @@ static void render(GB_gameboy_t *gb, bool no_downsampling, GB_sample_t *dest) unsigned mask = gb->io_registers[GB_IO_NR51]; unsigned left_volume = 0; unsigned right_volume = 0; - #pragma unroll + UNROLL for (unsigned i = GB_N_CHANNELS; i--;) { if (gb->apu.is_active[i]) { if (mask & 1) { @@ -145,37 +220,29 @@ static void render(GB_gameboy_t *gb, bool no_downsampling, GB_sample_t *dest) gb->apu_output.highpass_diff = (GB_double_sample_t) {left_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.left * gb->apu_output.highpass_rate, right_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.right * gb->apu_output.highpass_rate}; - + case GB_HIGHPASS_MAX:; } - - } - if (dest) { - *dest = filtered_output; - return; - } - while (gb->apu_output.copy_in_progress); - while (!__sync_bool_compare_and_swap(&gb->apu_output.lock, false, true)); - if (gb->apu_output.buffer_position < gb->apu_output.buffer_size) { - gb->apu_output.buffer[gb->apu_output.buffer_position++] = filtered_output; } - gb->apu_output.lock = false; + + assert(gb->apu_output.sample_callback); + gb->apu_output.sample_callback(gb, &filtered_output); } -static uint16_t new_sweep_sample_legnth(GB_gameboy_t *gb) +static uint16_t new_sweep_sample_length(GB_gameboy_t *gb) { - uint16_t delta = gb->apu.shadow_sweep_sample_legnth >> (gb->io_registers[GB_IO_NR10] & 7); + 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_legnth - delta; + return gb->apu.shadow_sweep_sample_length - delta; } - return gb->apu.shadow_sweep_sample_legnth + 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; - + uint8_t duty = gb->io_registers[index == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; update_sample(gb, index, duties[gb->apu.square_channels[index].current_sample_index + duty * 8]? @@ -194,38 +261,47 @@ static void nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value) if (value & 8) { (*volume)++; } - + if (((value ^ old_value) & 8)) { *volume = 0x10 - *volume; } - + if ((value & 7) && !(old_value & 7) && *volume && !(value & 8)) { (*volume)--; } - + if ((old_value & 7) && (value & 8)) { (*volume)--; } - + (*volume) &= 0xF; } 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); } @@ -236,19 +312,22 @@ static void tick_square_envelope(GB_gameboy_t *gb, enum GB_CHANNELS index) 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) { + 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++; } - + 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) ? @@ -262,11 +341,16 @@ static void tick_noise_envelope(GB_gameboy_t *gb) void GB_apu_div_event(GB_gameboy_t *gb) { if (!gb->apu.global_enable) return; - if (gb->apu.skip_div_event) { - gb->apu.skip_div_event = false; + if (gb->apu.skip_div_event == GB_SKIP_DIV_EVENT_SKIP) { + gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_SKIPPED; return; } - gb->apu.div_divider++; + if (gb->apu.skip_div_event == GB_SKIP_DIV_EVENT_SKIPPED) { + gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_INACTIVE; + } + else { + gb->apu.div_divider++; + } if ((gb->apu.div_divider & 1) == 0) { for (unsigned i = GB_SQUARE_2 + 1; i--;) { @@ -275,20 +359,20 @@ void GB_apu_div_event(GB_gameboy_t *gb) tick_square_envelope(gb, i); } } - + 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.div_divider & 7) == 0) { for (unsigned i = GB_SQUARE_2 + 1; i--;) { tick_square_envelope(gb, i); } - + tick_noise_envelope(gb); } - + if ((gb->apu.div_divider & 1) == 1) { for (unsigned i = GB_SQUARE_2 + 1; i--;) { if (gb->apu.square_channels[i].length_enabled) { @@ -300,17 +384,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) { gb->apu.is_active[GB_WAVE] = false; - gb->apu.wave_channel.current_sample = 0; update_sample(gb, GB_WAVE, 0, 0); } } } - + if (gb->apu.noise_channel.length_enabled) { if (gb->apu.noise_channel.pulse_length) { if (!--gb->apu.noise_channel.pulse_length) { @@ -320,7 +403,7 @@ void GB_apu_div_event(GB_gameboy_t *gb) } } } - + if ((gb->apu.div_divider & 3) == 3) { if (!gb->apu.sweep_enabled) { return; @@ -329,15 +412,15 @@ void GB_apu_div_event(GB_gameboy_t *gb) 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_legnth = - gb->apu.new_sweep_sample_legnth; + 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; } @@ -352,170 +435,130 @@ void GB_apu_run(GB_gameboy_t *gb) uint8_t cycles = gb->apu.apu_cycles >> 2; gb->apu.apu_cycles = 0; if (!cycles) return; - - /* 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 > 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_legnth = new_sweep_sample_legnth(gb); - if (gb->apu.new_sweep_sample_legnth > 0x7ff) { - 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.square_sweep_calculate_countdown = 0; - } - } - - #pragma unroll - for (unsigned i = GB_SQUARE_2 + 1; i--;) { - if (gb->apu.is_active[i]) { - uint8_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; - - update_square_sample(gb, i); - } - if (cycles_left) { - gb->apu.square_channels[i].sample_countdown -= cycles_left; - } - } - } - - gb->apu.wave_channel.wave_form_just_read = false; - if (gb->apu.is_active[GB_WAVE]) { - uint8_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.wave_form_just_read = true; - } - if (cycles_left) { - gb->apu.wave_channel.sample_countdown -= cycles_left; - 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; - /* Todo: is this formula is different on a GBA? */ - bool new_high_bit = (gb->apu.noise_channel.lfsr ^ (gb->apu.noise_channel.lfsr >> 1) ^ 1) & 1; - gb->apu.noise_channel.lfsr >>= 1; - - if (new_high_bit) { - gb->apu.noise_channel.lfsr |= high_bit_mask; + + if (likely(!gb->stopped || GB_is_cgb(gb))) { + /* To align the square signal to 1MHz */ + gb->apu.lf_div ^= cycles & 1; + gb->apu.noise_channel.alignment += cycles; + + if (gb->apu.square_sweep_calculate_countdown) { + if (gb->apu.square_sweep_calculate_countdown > cycles) { + gb->apu.square_sweep_calculate_countdown -= cycles; } else { - /* This code is not redundent, it's relevant when switching LFSR widths */ - gb->apu.noise_channel.lfsr &= ~high_bit_mask; + /* 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) { + 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.square_sweep_calculate_countdown = 0; } - - gb->apu.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; - if (gb->model == GB_MODEL_CGB_C) { - /* Todo: This was confirmed to happen on a CGB-C. This may or may not happen on pre-CGB models. - Because this degrades audio quality, and testing this on a pre-CGB device requires audio records, - I'll assume these devices are innocent until proven guilty. - - Also happens on CGB-B, but not on CGB-D. - */ - gb->apu.current_lfsr_sample &= gb->apu.previous_lfsr_sample; - } - gb->apu.previous_lfsr_sample = gb->apu.noise_channel.lfsr & 1; - - 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; + + UNROLL + for (unsigned i = GB_SQUARE_1; i <= GB_SQUARE_2; i++) { + if (gb->apu.is_active[i]) { + uint8_t cycles_left = cycles; + while (unlikely(cycles_left > gb->apu.square_channels[i].sample_countdown)) { + 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; + if (cycles_left == 0 && gb->apu.samples[i] == 0) { + gb->apu.pcm_mask[0] &= i == GB_SQUARE_1? 0xF0 : 0x0F; + } + + update_square_sample(gb, i); + } + if (cycles_left) { + gb->apu.square_channels[i].sample_countdown -= cycles_left; + } + } + } + + gb->apu.wave_channel.wave_form_just_read = false; + if (gb->apu.is_active[GB_WAVE]) { + uint8_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.wave_form_just_read = true; + } + if (cycles_left) { + gb->apu.wave_channel.sample_countdown -= cycles_left; + 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 { + /* This code is not redundent, it's relevant when switching LFSR widths */ + gb->apu.noise_channel.lfsr &= ~high_bit_mask; + } + + gb->apu.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; + + if (cycles_left == 0 && gb->apu.samples[GB_NOISE] == 0) { + gb->apu.pcm_mask[1] &= 0x0F; + } + + update_sample(gb, GB_NOISE, + gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + 0); + } + if (cycles_left) { + gb->apu.noise_channel.sample_countdown -= cycles_left; + } } } - + 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) { + + if (gb->apu_output.sample_cycles >= gb->apu_output.cycles_per_sample) { gb->apu_output.sample_cycles -= gb->apu_output.cycles_per_sample; - render(gb, false, NULL); + render(gb); } } } - -void GB_apu_copy_buffer(GB_gameboy_t *gb, GB_sample_t *dest, size_t count) -{ - if (gb->sgb) { - if (GB_sgb_render_jingle(gb, dest, count)) return; - } - gb->apu_output.copy_in_progress = true; - - /* TODO: Rewrite this as a proper cyclic buffer. This is a workaround to avoid a very rare crashing race condition */ - size_t buffer_position = gb->apu_output.buffer_position; - - if (!gb->apu_output.stream_started) { - // Intentionally fail the first copy to sync the stream with the Gameboy. - gb->apu_output.stream_started = true; - gb->apu_output.buffer_position = 0; - buffer_position = 0; - } - - if (count > buffer_position) { - // GB_log(gb, "Audio underflow: %d\n", count - gb->apu_output.buffer_position); - GB_sample_t output; - render(gb, true, &output); - - for (unsigned i = 0; i < count - buffer_position; i++) { - dest[buffer_position + i] = output; - } - - if (buffer_position) { - if (gb->apu_output.buffer_size + (count - buffer_position) < count * 3) { - gb->apu_output.buffer_size += count - buffer_position; - gb->apu_output.buffer = realloc(gb->apu_output.buffer, - gb->apu_output.buffer_size * sizeof(*gb->apu_output.buffer)); - gb->apu_output.stream_started = false; - } - } - count = buffer_position; - } - memcpy(dest, gb->apu_output.buffer, count * sizeof(*gb->apu_output.buffer)); - memmove(gb->apu_output.buffer, gb->apu_output.buffer + count, (buffer_position - count) * sizeof(*gb->apu_output.buffer)); - gb->apu_output.buffer_position -= count; - - gb->apu_output.copy_in_progress = false; -} - 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; /* 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)) { - gb->apu.skip_div_event = true; + gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_SKIP; + gb->apu.div_divider = 1; } } @@ -523,7 +566,7 @@ uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg) { if (reg == GB_IO_NR52) { uint8_t value = 0; - for (int i = 0; i < GB_N_CHANNELS; i++) { + for (unsigned i = 0; i < GB_N_CHANNELS; i++) { value >>= 1; if (gb->apu.is_active[i]) { value |= 0x8; @@ -561,14 +604,14 @@ uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg) void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) { - if (!gb->apu.global_enable && reg != GB_IO_NR52 && (GB_is_cgb(gb) || - ( - reg != GB_IO_NR11 && - reg != GB_IO_NR21 && - reg != GB_IO_NR31 && - reg != GB_IO_NR41 - ) - )) { + if (!gb->apu.global_enable && reg != GB_IO_NR52 && reg < GB_IO_WAV_START && (GB_is_cgb(gb) || + ( + reg != GB_IO_NR11 && + reg != GB_IO_NR21 && + reg != GB_IO_NR31 && + reg != GB_IO_NR41 + ) + )) { return; } @@ -578,7 +621,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } reg = GB_IO_WAV_START + gb->apu.wave_channel.current_sample_index / 2; } - + /* Todo: this can and should be rewritten with a function table. */ switch (reg) { /* Globals */ @@ -592,7 +635,7 @@ 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], @@ -611,10 +654,10 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) 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]); @@ -623,7 +666,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } } break; - + /* Square channels */ case GB_IO_NR10: if (gb->apu.sweep_decreasing && !(value & 8)) { @@ -638,7 +681,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.square_sweep_calculate_countdown = 0; } break; - + case GB_IO_NR11: case GB_IO_NR21: { unsigned index = reg == GB_IO_NR21? GB_SQUARE_2: GB_SQUARE_1; @@ -648,7 +691,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } break; } - + case GB_IO_NR12: case GB_IO_NR22: { unsigned index = reg == GB_IO_NR22? GB_SQUARE_2: GB_SQUARE_1; @@ -666,10 +709,10 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) nrx2_glitch(&gb->apu.square_channels[index].current_volume, value, gb->io_registers[reg]); update_square_sample(gb, index); } - + break; } - + case GB_IO_NR13: case GB_IO_NR23: { unsigned index = reg == GB_IO_NR23? GB_SQUARE_2: GB_SQUARE_1; @@ -677,15 +720,30 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.square_channels[index].sample_length |= value & 0xFF; break; } - + case GB_IO_NR14: case GB_IO_NR24: { unsigned index = reg == GB_IO_NR24? GB_SQUARE_2: GB_SQUARE_1; + + /* 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->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_length &= 0xFF; gb->apu.square_channels[index].sample_length |= (value & 7) << 8; if (index == GB_SQUARE_1) { - gb->apu.shadow_sweep_sample_legnth = - gb->apu.new_sweep_sample_legnth = + gb->apu.shadow_sweep_sample_length = + gb->apu.new_sweep_sample_length = gb->apu.square_channels[0].sample_length; } if (value & 0x80) { @@ -699,16 +757,16 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) 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].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. */ if (gb->apu.is_active[index]) { update_square_sample(gb, index); } - + gb->apu.square_channels[index].volume_countdown = gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] & 7; - + 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); @@ -719,7 +777,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.square_channels[index].pulse_length = 0x40; gb->apu.square_channels[index].length_enabled = false; } - + if (index == GB_SQUARE_1) { gb->apu.sweep_decreasing = false; if (gb->io_registers[GB_IO_NR10] & 7) { @@ -733,9 +791,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) 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; } - + } - + /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ if ((value & 0x40) && !gb->apu.square_channels[index].length_enabled && @@ -755,13 +813,12 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.square_channels[index].length_enabled = value & 0x40; break; } - + /* Wave channel */ case GB_IO_NR30: gb->apu.wave_channel.enable = value & 0x80; if (!gb->apu.wave_channel.enable) { gb->apu.is_active[GB_WAVE] = false; - gb->apu.wave_channel.current_sample = 0; update_sample(gb, GB_WAVE, 0, 0); } break; @@ -770,7 +827,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) break; case GB_IO_NR32: gb->apu.wave_channel.shift = (uint8_t[]){4, 0, 1, 2}[(value >> 5) & 3]; - update_sample(gb, GB_WAVE, gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, 0); + if (gb->apu.is_active[GB_WAVE]) { + update_sample(gb, GB_WAVE, gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, 0); + } break; case GB_IO_NR33: gb->apu.wave_channel.sample_length &= ~0xFF; @@ -787,12 +846,12 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.wave_channel.sample_countdown == 0 && gb->apu.wave_channel.enable) { 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. - + Additionally, I believe DMGs, including those we behave differently than emulated, are all deterministic. */ if (offset < 4) { @@ -811,7 +870,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } if (!gb->apu.is_active[GB_WAVE]) { gb->apu.is_active[GB_WAVE] = true; - update_sample(gb, GB_WAVE, 0, 0); + update_sample(gb, GB_WAVE, + gb->apu.wave_channel.current_sample >> 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; @@ -820,13 +881,8 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.wave_channel.length_enabled = false; } /* Note that we don't change the sample just yet! This was verified on hardware. */ - /* Todo: The first sample might *not* beskipped on the DMG, this could be a bug - introduced on the CGB. It appears that the bug was fixed on the AGB, but it's - not reflected by PCM34. This should be probably verified as this could just - mean differences in the DACs. */ - /* Todo: Similar issues may apply to the other channels on the DMG/AGB, test, verify and fix if needed */ } - + /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ if ((value & 0x40) && !gb->apu.wave_channel.length_enabled && @@ -850,14 +906,14 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } break; - + /* Noise Channel */ - + case GB_IO_NR41: { gb->apu.noise_channel.pulse_length = (0x40 - (value & 0x3f)); break; } - + case GB_IO_NR42: { if (((value & 0x7) == 0) && ((gb->io_registers[reg] & 0x7) != 0)) { /* Envelope disabled */ @@ -869,7 +925,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.is_active[GB_NOISE] = false; update_sample(gb, GB_NOISE, 0, 0); } - else if (gb->apu.is_active[GB_NOISE]){ + else if (gb->apu.is_active[GB_NOISE]) { nrx2_glitch(&gb->apu.noise_channel.current_volume, value, gb->io_registers[reg]); update_sample(gb, GB_NOISE, gb->apu.current_lfsr_sample ? @@ -878,24 +934,24 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } break; } - + 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. */ 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) { @@ -909,9 +965,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) if (gb->apu.is_active[GB_NOISE]) { gb->apu.noise_channel.sample_countdown += 2; } - + 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. */ @@ -924,18 +980,18 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) 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.noise_channel.pulse_length == 0) { gb->apu.noise_channel.pulse_length = 0x40; gb->apu.noise_channel.length_enabled = false; } } - + /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ if ((value & 0x40) && !gb->apu.noise_channel.length_enabled && @@ -955,7 +1011,7 @@ 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; @@ -965,26 +1021,35 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->io_registers[reg] = value; } -size_t GB_apu_get_current_buffer_length(GB_gameboy_t *gb) +void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate) { - return gb->apu_output.buffer_position; -} -void GB_set_sample_rate(GB_gameboy_t *gb, unsigned int sample_rate) -{ - if (gb->apu_output.buffer) { - free(gb->apu_output.buffer); - } - gb->apu_output.buffer_size = sample_rate / 25; // 40ms delay - gb->apu_output.buffer = malloc(gb->apu_output.buffer_size * sizeof(*gb->apu_output.buffer)); gb->apu_output.sample_rate = sample_rate; - gb->apu_output.buffer_position = 0; 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) +{ + + if (cycles_per_sample == 0) { + 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) +{ + gb->apu_output.sample_callback = callback; +} + void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode) { gb->apu_output.highpass_mode = mode; @@ -992,6 +1057,7 @@ void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode) void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb) { + 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 */ } diff --git a/Core/apu.h b/Core/apu.h index bfa3598..a3a36a6 100644 --- a/Core/apu.h +++ b/Core/apu.h @@ -8,12 +8,8 @@ #ifdef GB_INTERNAL /* Speed = 1 / Length (in seconds) */ -/* Todo: Measure these and find the actual curve shapes. - They are known to be incorrect (Some analog test ROM sound different), - but are good enough approximations to fix Cannon Fodder's terrible audio. - It also varies by model. */ -#define DAC_DECAY_SPEED 50000 -#define DAC_ATTACK_SPEED 1000 +#define DAC_DECAY_SPEED 20000 +#define DAC_ATTACK_SPEED 20000 /* Divides nicely and never overflows with 4 channels and 8 (1-8) volume levels */ @@ -50,27 +46,29 @@ enum GB_CHANNELS { GB_N_CHANNELS }; +typedef void (*GB_sample_callback_t)(GB_gameboy_t *gb, GB_sample_t *sample); + typedef struct { bool global_enable; uint8_t apu_cycles; - + uint8_t samples[GB_N_CHANNELS]; bool is_active[GB_N_CHANNELS]; - + uint8_t div_divider; // The DIV register ticks the APU at 512Hz, but is then divided // once more to generate 128Hz and 64Hz clocks - + uint8_t lf_div; // The APU runs in 2MHz, but channels 1, 2 and 4 run in 1MHZ so we divide // need to divide the signal. - + uint8_t square_sweep_countdown; // In 128Hz uint8_t square_sweep_calculate_countdown; // In 2 MHz - uint16_t new_sweep_sample_legnth; - uint16_t shadow_sweep_sample_legnth; + uint16_t new_sweep_sample_length; + uint16_t shadow_sweep_sample_length; bool sweep_enabled; bool sweep_decreasing; - + struct { uint16_t pulse_length; // Reloaded from NRX1 (xorred), in 256Hz DIV ticks uint8_t current_volume; // Reloaded from NRX2 @@ -78,47 +76,50 @@ typedef struct uint8_t current_sample_index; /* For save state compatibility, highest bit is reused (See NR14/NR24's write code)*/ - + 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 } square_channels[2]; - + struct { bool enable; // NR30 uint16_t pulse_length; // Reloaded from NR31 (xorred), in 256Hz DIV ticks uint8_t shift; // NR32 uint16_t sample_length; // NR33, NR34, in APU ticks bool length_enabled; // NR34 - + 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]; bool wave_form_just_read; } wave_channel; - + struct { uint16_t pulse_length; // Reloaded from NR41 (xorred), in 256Hz DIV ticks uint8_t current_volume; // Reloaded from NR42 uint8_t volume_countdown; // Reloaded from NR42 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 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. - + } noise_channel; - - bool skip_div_event; + +#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; - bool previous_lfsr_sample; + uint8_t pcm_mask[2]; // For CGB-0 to CGB-C PCM read glitch } GB_apu_t; typedef enum { @@ -130,42 +131,39 @@ typedef enum { typedef struct { unsigned sample_rate; - - GB_sample_t *buffer; - size_t buffer_size; - size_t buffer_position; - - bool stream_started; /* detects first copy request to minimize lag */ - volatile bool copy_in_progress; - volatile bool lock; - + double sample_cycles; // In 8 MHz units double cycles_per_sample; - + // Samples are NOT normalized to MAX_CH_AMP * 4 at this stage! unsigned cycles_since_render; unsigned last_update[GB_N_CHANNELS]; GB_sample_t current_sample[GB_N_CHANNELS]; GB_sample_t summed_samples[GB_N_CHANNELS]; double dac_discharge[GB_N_CHANNELS]; - + GB_highpass_mode_t highpass_mode; double highpass_rate; GB_double_sample_t highpass_diff; + + GB_sample_callback_t sample_callback; + + bool rate_set_in_clocks; } GB_apu_output_t; -void GB_set_sample_rate(GB_gameboy_t *gb, unsigned int sample_rate); -void GB_apu_copy_buffer(GB_gameboy_t *gb, GB_sample_t *dest, size_t count); -size_t GB_apu_get_current_buffer_length(GB_gameboy_t *gb); +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_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); #endif #endif /* apu_h */ diff --git a/Core/camera.c b/Core/camera.c index 9b34998..bef8489 100644 --- a/Core/camera.c +++ b/Core/camera.c @@ -1,17 +1,17 @@ #include "gb.h" -static int noise_seed = 0; +static signed noise_seed = 0; /* This is not a complete emulation of the camera chip. Only the features used by the GameBoy 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) { - int value = (x + y * 128 + noise_seed); + signed value = (x + y * 128 + noise_seed); uint8_t *data = (uint8_t *) &value; unsigned hash = 0; - while ((int *) data != &value + 1) { + while ((signed *) data != &value + 1) { hash ^= (*data << 8); if (hash & 0x8000) { hash ^= 0x8a00; diff --git a/Core/cheats.c b/Core/cheats.c new file mode 100644 index 0000000..14875e0 --- /dev/null +++ b/Core/cheats.c @@ -0,0 +1,313 @@ +#include "gb.h" +#include "cheats.h" +#include +#include +#include + +static inline uint8_t hash_addr(uint16_t addr) +{ + return addr; +} + +static uint16_t bank_for_addr(GB_gameboy_t *gb, uint16_t addr) +{ + if (addr < 0x4000) { + return gb->mbc_rom0_bank; + } + + if (addr < 0x8000) { + return gb->mbc_rom_bank; + } + + if (addr < 0xD000) { + return 0; + } + + if (addr < 0xE000) { + return gb->cgb_ram_bank; + } + + return 0; +} + +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; + const GB_cheat_hash_t *hash = gb->cheat_hash[hash_addr(address)]; + if (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)) { + if (cheat->bank == GB_CHEAT_ANY_BANK || cheat->bank == bank_for_addr(gb, address)) { + *value = cheat->value; + break; + } + } + } + } +} + +bool GB_cheats_enabled(GB_gameboy_t *gb) +{ + return gb->cheat_enabled; +} + +void GB_set_cheats_enabled(GB_gameboy_t *gb, bool enabled) +{ + gb->cheat_enabled = enabled; +} + +void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled) +{ + GB_cheat_t *cheat = malloc(sizeof(*cheat)); + cheat->address = address; + cheat->bank = bank; + cheat->value = value; + cheat->old_value = old_value; + cheat->use_old_value = use_old_value; + 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[gb->cheat_count - 1] = cheat; + + GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(address)]; + if (!*hash) { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat)); + (*hash)->size = 1; + (*hash)->cheats[0] = cheat; + } + else { + (*hash)->size++; + *hash = realloc(*hash, sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + (*hash)->cheats[(*hash)->size - 1] = cheat; + } +} + +const GB_cheat_t *const *GB_get_cheats(GB_gameboy_t *gb, size_t *size) +{ + *size = gb->cheat_count; + return (void *)gb->cheats; +} +void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat) +{ + for (unsigned i = 0; i < gb->cheat_count; i++) { + if (gb->cheats[i] == cheat) { + gb->cheats[i] = gb->cheats[--gb->cheat_count]; + if (gb->cheat_count == 0) { + free(gb->cheats); + gb->cheats = NULL; + } + else { + gb->cheats = realloc(gb->cheats, gb->cheat_count * sizeof(*cheat)); + } + break; + } + } + + 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--]; + if ((*hash)->size == 0) { + free(*hash); + *hash = NULL; + } + else { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + } + break; + } + } + + free((void *)cheat); +} + +bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled) +{ + uint8_t dummy; + /* GameShark */ + { + uint8_t bank; + uint8_t value; + uint16_t address; + if (sscanf(cheat, "%02hhx%02hhx%04hx%c", &bank, &value, &address, &dummy) == 3) { + if (bank >= 0x80) { + bank &= 0xF; + } + GB_add_cheat(gb, description, address, bank, value, 0, false, enabled); + return true; + } + } + + /* GameGenie */ + { + char stripped_cheat[10] = {0,}; + for (unsigned i = 0; i < 9 && *cheat; i++) { + stripped_cheat[i] = *(cheat++); + while (*cheat == '-') { + cheat++; + } + } + + // Delete the 7th character; + stripped_cheat[7] = stripped_cheat[8]; + stripped_cheat[8] = 0; + + uint8_t old_value; + uint8_t value; + uint16_t address; + if (sscanf(stripped_cheat, "%02hhx%04hx%02hhx%c", &value, &address, &old_value, &dummy) == 3) { + address = (uint16_t)(address >> 4) | (uint16_t)(address << 12); + address ^= 0xF000; + if (address > 0x7FFF) { + return false; + } + old_value = (uint8_t)(old_value >> 2) | (uint8_t)(old_value << 6); + old_value ^= 0xBA; + GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, old_value, true, enabled); + return true; + } + + if (sscanf(stripped_cheat, "%02hhx%04hx%c", &value, &address, &dummy) == 2) { + address = (uint16_t)(address >> 4) | (uint16_t)(address << 12); + address ^= 0xF000; + if (address > 0x7FFF) { + return false; + } + GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, false, true, enabled); + return true; + } + } + return false; +} + +void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled) +{ + GB_cheat_t *cheat = NULL; + for (unsigned i = 0; i < gb->cheat_count; i++) { + if (gb->cheats[i] == _cheat) { + cheat = gb->cheats[i]; + break; + } + } + + assert(cheat); + + if (cheat->address != address) { + /* Remove from old bucket */ + 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--]; + if ((*hash)->size == 0) { + free(*hash); + *hash = NULL; + } + else { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + } + break; + } + } + cheat->address = address; + + /* Add to new bucket */ + hash = &gb->cheat_hash[hash_addr(address)]; + if (!*hash) { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat)); + (*hash)->size = 1; + (*hash)->cheats[0] = cheat; + } + else { + (*hash)->size++; + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + (*hash)->cheats[(*hash)->size - 1] = cheat; + } + } + cheat->bank = bank; + cheat->value = value; + cheat->old_value = old_value; + cheat->use_old_value = use_old_value; + cheat->enabled = enabled; + if (description != cheat->description) { + strncpy(cheat->description, description, sizeof(cheat->description)); + cheat->description[sizeof(cheat->description) - 1] = 0; + } +} + +#define CHEAT_MAGIC 'SBCh' + +void GB_load_cheats(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + return; + } + + uint32_t magic = 0; + 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)) { + GB_log(gb, "The file is not a SameBoy cheat database"); + return; + } + + if (struct_size != sizeof(GB_cheat_t)) { + GB_log(gb, "This cheat database is not compatible with this version of SameBoy"); + return; + } + + // Remove all cheats first + while (gb->cheats) { + GB_remove_cheat(gb, gb->cheats[0]); + } + + GB_cheat_t cheat; + while (fread(&cheat, sizeof(cheat), 1, f)) { + if (magic == __builtin_bswap32(CHEAT_MAGIC)) { + cheat.address = __builtin_bswap16(cheat.address); + cheat.bank = __builtin_bswap16(cheat.bank); + } + cheat.description[sizeof(cheat.description) - 1] = 0; + GB_add_cheat(gb, cheat.description, cheat.address, cheat.bank, cheat.value, cheat.old_value, cheat.use_old_value, cheat.enabled); + } + + return; +} + +int GB_save_cheats(GB_gameboy_t *gb, const char *path) +{ + if (!gb->cheat_count) return 0; // Nothing to save. + FILE *f = fopen(path, "wb"); + if (!f) { + GB_log(gb, "Could not dump cheat database: %s.\n", strerror(errno)); + return errno; + } + + uint32_t magic = CHEAT_MAGIC; + uint32_t struct_size = sizeof(GB_cheat_t); + + if (fwrite(&magic, sizeof(magic), 1, f) != 1) { + fclose(f); + return EIO; + } + + if (fwrite(&struct_size, sizeof(struct_size), 1, f) != 1) { + fclose(f); + return EIO; + } + + for (size_t i = 0; i cheat_count; i++) { + if (fwrite(gb->cheats[i], sizeof(*gb->cheats[i]), 1, f) != 1) { + fclose(f); + return EIO; + } + } + + errno = 0; + fclose(f); + return errno; +} diff --git a/Core/cheats.h b/Core/cheats.h new file mode 100644 index 0000000..cf8aa20 --- /dev/null +++ b/Core/cheats.h @@ -0,0 +1,42 @@ +#ifndef cheats_h +#define cheats_h +#include "gb_struct_def.h" + +#define GB_CHEAT_ANY_BANK 0xFFFF + +typedef struct GB_cheat_s GB_cheat_t; + +void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled); +void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled); +bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled); +const GB_cheat_t *const *GB_get_cheats(GB_gameboy_t *gb, size_t *size); +void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat); +bool GB_cheats_enabled(GB_gameboy_t *gb); +void GB_set_cheats_enabled(GB_gameboy_t *gb, bool enabled); +void GB_load_cheats(GB_gameboy_t *gb, const char *path); +int GB_save_cheats(GB_gameboy_t *gb, const char *path); + +#ifdef GB_INTERNAL +#ifdef GB_DISABLE_CHEATS +#define GB_apply_cheat(...) +#else +void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value); +#endif +#endif + +typedef struct { + size_t size; + GB_cheat_t *cheats[]; +} GB_cheat_hash_t; + +struct GB_cheat_s { + uint16_t address; + uint16_t bank; + uint8_t value; + uint8_t old_value; + bool use_old_value; + bool enabled; + char description[128]; +}; + +#endif diff --git a/Core/debugger.c b/Core/debugger.c index 7ea3a3f..038f76f 100644 --- a/Core/debugger.c +++ b/Core/debugger.c @@ -214,13 +214,14 @@ static value_t read_lvalue(GB_gameboy_t *gb, lvalue_t lvalue) return r; } return VALUE_16(GB_read_memory(gb, lvalue.memory_address.value)); - + case LVALUE_MEMORY16: if (lvalue.memory_address.has_bank) { banking_state_t state; save_banking_state(gb, &state); switch_banking_state(gb, lvalue.memory_address.bank); - value_t r = VALUE_16(GB_read_memory(gb, lvalue.memory_address.value)); + value_t r = VALUE_16(GB_read_memory(gb, lvalue.memory_address.value) | + (GB_read_memory(gb, lvalue.memory_address.value + 1) * 0x100)); restore_banking_state(gb, &state); return r; } @@ -254,13 +255,14 @@ static void write_lvalue(GB_gameboy_t *gb, lvalue_t lvalue, uint16_t value) } GB_write_memory(gb, lvalue.memory_address.value, value); return; - + case LVALUE_MEMORY16: if (lvalue.memory_address.has_bank) { banking_state_t state; save_banking_state(gb, &state); switch_banking_state(gb, lvalue.memory_address.bank); GB_write_memory(gb, lvalue.memory_address.value, value); + GB_write_memory(gb, lvalue.memory_address.value + 1, value >> 8); restore_banking_state(gb, &state); return; } @@ -288,7 +290,7 @@ static void write_lvalue(GB_gameboy_t *gb, lvalue_t lvalue, uint16_t value) 25 bit address 16 bit value = 25 bit address 16 bit value 25 bit address = 25 bit address 25 bit address 25 bit address = 16 bit value (since adding pointers, for examples, makes no sense) - + Boolean operators always return a 16-bit value */ #define FIX_BANK(x) ((value_t){a.has_bank ^ b.has_bank, a.has_bank? a.bank : b.bank, (x)}) @@ -296,13 +298,15 @@ static void write_lvalue(GB_gameboy_t *gb, lvalue_t lvalue, uint16_t value) static value_t add(value_t a, value_t b) {return FIX_BANK(a.value + b.value);} static value_t sub(value_t a, value_t b) {return FIX_BANK(a.value - b.value);} static value_t mul(value_t a, value_t b) {return FIX_BANK(a.value * b.value);} -static value_t _div(value_t a, value_t b) { +static value_t _div(value_t a, value_t b) +{ if (b.value == 0) { return FIX_BANK(0); } return FIX_BANK(a.value / b.value); }; -static value_t mod(value_t a, value_t b) { +static value_t mod(value_t a, value_t b) +{ if (b.value == 0) { return FIX_BANK(0); } @@ -332,7 +336,7 @@ static value_t bank(value_t a, value_t b) {return (value_t) {true, a.value, b.va static struct { const char *string; - char priority; + int8_t priority; value_t (*operator)(value_t, value_t); value_t (*lvalue_operator)(GB_gameboy_t *, lvalue_t, uint16_t); } operators[] = @@ -378,16 +382,15 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, while (length && (string[length-1] == ' ' || string[length-1] == '\n' || string[length-1] == '\r' || string[length-1] == '\t')) { length--; } - if (length == 0) - { + if (length == 0) { GB_log(gb, "Expected expression.\n"); *error = true; return (lvalue_t){0,}; } if (string[0] == '(' && string[length - 1] == ')') { // Attempt to strip parentheses - signed int depth = 0; - for (int i = 0; i < length; i++) { + signed depth = 0; + for (unsigned i = 0; i < length; i++) { if (string[i] == '(') depth++; if (depth == 0) { // First and last are not matching @@ -400,8 +403,8 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, } else if (string[0] == '[' && string[length - 1] == ']') { // Attempt to strip square parentheses (memory dereference) - signed int depth = 0; - for (int i = 0; i < length; i++) { + signed depth = 0; + for (unsigned i = 0; i < length; i++) { if (string[i] == '[') depth++; if (depth == 0) { // First and last are not matching @@ -416,8 +419,8 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, } else if (string[0] == '{' && string[length - 1] == '}') { // Attempt to strip curly parentheses (memory dereference) - signed int depth = 0; - for (int i = 0; i < length; i++) { + signed depth = 0; + for (unsigned i = 0; i < length; i++) { if (string[i] == '{') depth++; if (depth == 0) { // First and last are not matching @@ -455,12 +458,12 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, case 'p': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->pc}; } } - GB_log(gb, "Unknown register: %.*s\n", (unsigned int) length, string); + GB_log(gb, "Unknown register: %.*s\n", (unsigned) length, string); *error = true; return (lvalue_t){0,}; } - GB_log(gb, "Expression is not an lvalue: %.*s\n", (unsigned int) length, string); + GB_log(gb, "Expression is not an lvalue: %.*s\n", (unsigned) length, string); *error = true; return (lvalue_t){0,}; } @@ -473,9 +476,9 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, /* Disable watchpoints while evaulating expressions */ uint16_t n_watchpoints = gb->n_watchpoints; gb->n_watchpoints = 0; - + value_t ret = ERROR; - + *error = false; // Strip whitespace while (length && (string[0] == ' ' || string[0] == '\n' || string[0] == '\r' || string[0] == '\t')) { @@ -485,16 +488,15 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, while (length && (string[length-1] == ' ' || string[length-1] == '\n' || string[length-1] == '\r' || string[length-1] == '\t')) { length--; } - if (length == 0) - { + if (length == 0) { GB_log(gb, "Expected expression.\n"); *error = true; goto exit; } if (string[0] == '(' && string[length - 1] == ')') { // Attempt to strip parentheses - signed int depth = 0; - for (int i = 0; i < length; i++) { + signed depth = 0; + for (unsigned i = 0; i < length; i++) { if (string[i] == '(') depth++; if (depth == 0) { // First and last are not matching @@ -510,8 +512,8 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } else if (string[0] == '[' && string[length - 1] == ']') { // Attempt to strip square parentheses (memory dereference) - signed int depth = 0; - for (int i = 0; i < length; i++) { + signed depth = 0; + for (unsigned i = 0; i < length; i++) { if (string[i] == '[') depth++; if (depth == 0) { // First and last are not matching @@ -537,8 +539,8 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } else if (string[0] == '{' && string[length - 1] == '}') { // Attempt to strip curly parentheses (memory dereference) - signed int depth = 0; - for (int i = 0; i < length; i++) { + signed depth = 0; + for (unsigned i = 0; i < length; i++) { if (string[i] == '{') depth++; if (depth == 0) { // First and last are not matching @@ -547,7 +549,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } if (string[i] == '}') depth--; } - + if (depth == 0) { value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); banking_state_t state; @@ -563,21 +565,25 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } } // Search for lowest priority operator - signed int depth = 0; - unsigned int operator_index = -1; - unsigned int operator_pos = 0; - for (int i = 0; i < length; i++) { + signed depth = 0; + unsigned operator_index = -1; + unsigned operator_pos = 0; + for (unsigned i = 0; i < length; i++) { if (string[i] == '(') depth++; else if (string[i] == ')') depth--; else if (string[i] == '[') depth++; else if (string[i] == ']') depth--; else if (depth == 0) { - for (int j = 0; j < sizeof(operators) / sizeof(operators[0]); j++) { - if (strlen(operators[j].string) > length - i) continue; // Operator too big. - // Priority higher than what we already have. - if (operator_index != -1 && operators[operator_index].priority < operators[j].priority) continue; - unsigned long operator_length = strlen(operators[j].string); + for (unsigned j = 0; j < sizeof(operators) / sizeof(operators[0]); j++) { + unsigned operator_length = strlen(operators[j].string); + if (operator_length > length - i) continue; // Operator too long + if (memcmp(string + i, operators[j].string, operator_length) == 0) { + if (operator_index != -1 && operators[operator_index].priority < operators[j].priority) { + /* for supporting = vs ==, etc*/ + i += operator_length - 1; + break; + } // Found an operator! operator_pos = i; operator_index = j; @@ -589,7 +595,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } } if (operator_index != -1) { - unsigned int right_start = (unsigned int)(operator_pos + strlen(operators[operator_index].string)); + unsigned right_start = (unsigned)(operator_pos + strlen(operators[operator_index].string)); value_t right = debugger_evaluate(gb, string + right_start, length - right_start, error, watchpoint_address, watchpoint_new_value); if (*error) goto exit; if (operators[operator_index].lvalue_operator) { @@ -657,13 +663,13 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, goto exit; } - GB_log(gb, "Unknown register or symbol: %.*s\n", (unsigned int) length, string); + GB_log(gb, "Unknown register or symbol: %.*s\n", (unsigned) length, string); *error = true; goto exit; } char *end; - int base = 10; + unsigned base = 10; if (string[0] == '$') { string++; base = 16; @@ -671,7 +677,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } uint16_t literal = (uint16_t) (strtol(string, &end, base)); if (end != string + length) { - GB_log(gb, "Failed to parse: %.*s\n", (unsigned int) length, string); + GB_log(gb, "Failed to parse: %.*s\n", (unsigned) length, string); *error = true; goto exit; } @@ -683,6 +689,7 @@ exit: struct debugger_command_s; typedef bool debugger_command_imp_t(GB_gameboy_t *gb, char *arguments, char *modifiers, const struct debugger_command_s *command); +typedef char *debugger_completer_imp_t(GB_gameboy_t *gb, const char *string, uintptr_t *context); typedef struct debugger_command_s { const char *command; @@ -691,6 +698,8 @@ typedef struct debugger_command_s { const char *help_string; // Null if should not appear in help const char *arguments_format; // For usage message const char *modifiers_format; // For usage message + debugger_completer_imp_t *argument_completer; + debugger_completer_imp_t *modifiers_completer; } debugger_command_t; static const char *lstrip(const char *str) @@ -751,7 +760,7 @@ static bool next(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug print_usage(gb, command); return true; } - + gb->debug_stopped = false; gb->debug_next_command = true; gb->debug_call_depth = 0; @@ -791,7 +800,7 @@ static bool stack_leak_detection(GB_gameboy_t *gb, char *arguments, char *modifi { NO_MODIFIERS STOPPED_ONLY - + if (strlen(lstrip(arguments))) { print_usage(gb, command); return true; @@ -811,12 +820,48 @@ static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const return true; } - GB_log(gb, "AF = $%04x\n", gb->registers[GB_REGISTER_AF]); /* AF can't really be an address */ - 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, "PC = %s\n", value_to_string(gb, gb->pc, false)); + + GB_log(gb, "AF = $%04x (%c%c%c%c)\n", gb->registers[GB_REGISTER_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, "PC = %s\n", value_to_string(gb, gb->pc, false)); + GB_log(gb, "IME = %s\n", gb->ime? "Enabled" : "Disabled"); + return true; +} + +static char *on_off_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"on", "off"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +/* Enable or disable software breakpoints */ +static bool softbreak(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strcmp(lstrip(arguments), "on") == 0 || !strlen(lstrip(arguments))) { + gb->has_software_breakpoints = true; + } + else if (strcmp(lstrip(arguments), "off") == 0) { + gb->has_software_breakpoints = false; + } + else { + print_usage(gb, command); + } + return true; } @@ -829,8 +874,8 @@ static uint16_t find_breakpoint(GB_gameboy_t *gb, value_t addr) uint32_t key = BP_KEY(addr); - int min = 0; - int max = gb->n_breakpoints; + unsigned min = 0; + unsigned max = gb->n_breakpoints; while (min < max) { uint16_t pivot = (min + max) / 2; if (gb->breakpoints[pivot].key == key) return pivot; @@ -844,6 +889,65 @@ static uint16_t find_breakpoint(GB_gameboy_t *gb, value_t addr) return (uint16_t) min; } +static inline bool is_legal_symbol_char(char c) +{ + if (c >= '0' && c <= '9') return true; + if (c >= 'A' && c <= 'Z') return true; + if (c >= 'a' && c <= 'z') return true; + if (c == '_') return true; + if (c == '.') return true; + return false; +} + +static char *symbol_completer(GB_gameboy_t *gb, const char *string, uintptr_t *_context) +{ + const char *symbol_prefix = string; + while (*string) { + if (!is_legal_symbol_char(*string)) { + symbol_prefix = string + 1; + } + string++; + } + + if (*symbol_prefix == '$') { + return NULL; + } + + struct { + uint16_t bank; + uint32_t symbol; + } *context = (void *)_context; + + + size_t length = strlen(symbol_prefix); + while (context->bank < 0x200) { + if (gb->bank_symbols[context->bank] == NULL || + context->symbol >= gb->bank_symbols[context->bank]->n_symbols) { + context->bank++; + context->symbol = 0; + continue; + } + const char *candidate = gb->bank_symbols[context->bank]->symbols[context->symbol++].name; + if (memcmp(symbol_prefix, candidate, length) == 0) { + return strdup(candidate + length); + } + } + return NULL; +} + +static char *j_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"j"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { bool is_jump_to = true; @@ -854,7 +958,7 @@ static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const print_usage(gb, command); return true; } - + if (strlen(lstrip(arguments)) == 0) { print_usage(gb, command); return true; @@ -871,13 +975,13 @@ static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const condition += strlen(" if "); /* Verify condition is sane (Todo: This might have side effects!) */ bool error; - debugger_evaluate(gb, condition, (unsigned int)strlen(condition), &error, NULL, NULL); + debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, NULL, NULL); if (error) return true; } bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL); + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); uint32_t key = BP_KEY(result); if (error) return true; @@ -913,9 +1017,9 @@ static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const gb->breakpoints[index].condition = NULL; } gb->n_breakpoints++; - + gb->breakpoints[index].is_jump_to = is_jump_to; - + if (is_jump_to) { gb->has_jump_to_breakpoints = true; } @@ -940,7 +1044,7 @@ static bool delete(GB_gameboy_t *gb, char *arguments, char *modifiers, const deb } bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL); + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); uint32_t key = BP_KEY(result); if (error) return true; @@ -957,7 +1061,7 @@ static bool delete(GB_gameboy_t *gb, char *arguments, char *modifiers, const deb index = i; } } - + if (index >= gb->n_breakpoints) { GB_log(gb, "No breakpoint set at %s\n", debugger_value_to_string(gb, result, true)); return true; @@ -965,7 +1069,7 @@ static bool delete(GB_gameboy_t *gb, char *arguments, char *modifiers, const deb result.bank = gb->breakpoints[index].bank; result.has_bank = gb->breakpoints[index].bank != (uint16_t) -1; - + if (gb->breakpoints[index].condition) { free(gb->breakpoints[index].condition); } @@ -980,7 +1084,7 @@ static bool delete(GB_gameboy_t *gb, char *arguments, char *modifiers, const deb } } } - + memmove(&gb->breakpoints[index], &gb->breakpoints[index + 1], (gb->n_breakpoints - index - 1) * sizeof(gb->breakpoints[0])); gb->n_breakpoints--; gb->breakpoints = realloc(gb->breakpoints, gb->n_breakpoints * sizeof(gb->breakpoints[0])); @@ -996,8 +1100,8 @@ static uint16_t find_watchpoint(GB_gameboy_t *gb, value_t addr) return 0; } uint32_t key = WP_KEY(addr); - int min = 0; - int max = gb->n_watchpoints; + unsigned min = 0; + unsigned max = gb->n_watchpoints; while (min < max) { uint16_t pivot = (min + max) / 2; if (gb->watchpoints[pivot].key == key) return pivot; @@ -1011,6 +1115,19 @@ static uint16_t find_watchpoint(GB_gameboy_t *gb, value_t addr) return (uint16_t) min; } +static char *rw_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"r", "rw", "w"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool watch(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { if (strlen(lstrip(arguments)) == 0) { @@ -1056,13 +1173,13 @@ print_usage: /* To make $new and $old legal */ uint16_t dummy = 0; uint8_t dummy2 = 0; - debugger_evaluate(gb, condition, (unsigned int)strlen(condition), &error, &dummy, &dummy2); + debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, &dummy, &dummy2); if (error) return true; } bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL); + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); uint32_t key = WP_KEY(result); if (error) return true; @@ -1123,7 +1240,7 @@ static bool unwatch(GB_gameboy_t *gb, char *arguments, char *modifiers, const de } bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL); + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); uint32_t key = WP_KEY(result); if (error) return true; @@ -1140,12 +1257,12 @@ static bool unwatch(GB_gameboy_t *gb, char *arguments, char *modifiers, const de index = i; } } - + if (index >= gb->n_watchpoints) { GB_log(gb, "No watchpoint set at %s\n", debugger_value_to_string(gb, result, true)); return true; } - + result.bank = gb->watchpoints[index].bank; result.has_bank = gb->watchpoints[index].bank != (uint16_t) -1; @@ -1155,7 +1272,7 @@ static bool unwatch(GB_gameboy_t *gb, char *arguments, char *modifiers, const de memmove(&gb->watchpoints[index], &gb->watchpoints[index + 1], (gb->n_watchpoints - index - 1) * sizeof(gb->watchpoints[0])); gb->n_watchpoints--; - gb->watchpoints = realloc(gb->watchpoints, gb->n_watchpoints* sizeof(gb->watchpoints[0])); + gb->watchpoints = realloc(gb->watchpoints, gb->n_watchpoints *sizeof(gb->watchpoints[0])); GB_log(gb, "Watchpoint removed from %s\n", debugger_value_to_string(gb, result, true)); return true; @@ -1204,7 +1321,7 @@ static bool list(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug gb->watchpoints[i].condition); } else { - GB_log(gb, " %d. %s (%c%c)\n", i + 1, debugger_value_to_string(gb,addr, addr.has_bank), + GB_log(gb, " %d. %s (%c%c)\n", i + 1, debugger_value_to_string(gb, addr, addr.has_bank), (gb->watchpoints[i].flags & GB_WATCHPOINT_R)? 'r' : '-', (gb->watchpoints[i].flags & GB_WATCHPOINT_W)? 'w' : '-'); } @@ -1225,7 +1342,7 @@ static bool _should_break(GB_gameboy_t *gb, value_t addr, bool jump_to) } bool error; bool condition = debugger_evaluate(gb, gb->breakpoints[index].condition, - (unsigned int)strlen(gb->breakpoints[index].condition), &error, NULL, NULL).value; + (unsigned)strlen(gb->breakpoints[index].condition), &error, NULL, NULL).value; if (error) { /* Should never happen */ GB_log(gb, "An internal error has occured\n"); @@ -1248,6 +1365,19 @@ static bool should_break(GB_gameboy_t *gb, uint16_t addr, bool jump_to) return _should_break(gb, full_addr, jump_to); } +static char *format_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"a", "b", "d", "o", "x"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { if (strlen(lstrip(arguments)) == 0) { @@ -1264,7 +1394,7 @@ static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu } bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL); + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); if (!error) { switch (modifiers[0]) { case 'a': @@ -1310,7 +1440,7 @@ static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const de } bool error; - value_t addr = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL); + value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); uint16_t count = 32; if (modifiers) { @@ -1330,7 +1460,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 (int i = 0; i < 16 && count; i++) { + for (unsigned i = 0; i < 16 && count; i++) { GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); count--; } @@ -1343,7 +1473,7 @@ static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const de else { while (count) { GB_log(gb, "%04x: ", addr.value); - for (int i = 0; i < 16 && count; i++) { + for (unsigned i = 0; i < 16 && count; i++) { GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); count--; } @@ -1362,7 +1492,7 @@ static bool disassemble(GB_gameboy_t *gb, char *arguments, char *modifiers, cons } bool error; - value_t addr = debugger_evaluate(gb, arguments, (unsigned int)strlen(arguments), &error, NULL, NULL); + value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); uint16_t count = 5; if (modifiers) { @@ -1410,19 +1540,26 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg } if (cartridge->mbc_type) { - static const char * const mapper_names[] = { - [GB_MBC1] = "MBC1", - [GB_MBC2] = "MBC2", - [GB_MBC3] = "MBC3", - [GB_MBC5] = "MBC5", - [GB_HUC1] = "HUC1", - [GB_HUC3] = "HUC3", - }; - GB_log(gb, "%s\n", mapper_names[cartridge->mbc_type]); + if (gb->is_mbc30) { + GB_log(gb, "MBC30\n"); + } + else { + static const char *const mapper_names[] = { + [GB_MBC1] = "MBC1", + [GB_MBC2] = "MBC2", + [GB_MBC3] = "MBC3", + [GB_MBC5] = "MBC5", + [GB_HUC1] = "HUC-1", + [GB_HUC3] = "HUC-3", + }; + GB_log(gb, "%s\n", mapper_names[cartridge->mbc_type]); + } GB_log(gb, "Current mapped ROM bank: %x\n", gb->mbc_rom_bank); if (cartridge->has_ram) { GB_log(gb, "Current mapped RAM bank: %x\n", gb->mbc_ram_bank); - GB_log(gb, "RAM is curently %s\n", gb->mbc_ram_enable? "enabled" : "disabled"); + if (gb->cartridge_type->mbc_type != GB_HUC1) { + GB_log(gb, "RAM is curently %s\n", gb->mbc_ram_enable? "enabled" : "disabled"); + } } if (cartridge->mbc_type == GB_MBC1 && gb->mbc1_wiring == GB_STANDARD_MBC1_WIRING) { GB_log(gb, "MBC1 banking mode is %s\n", gb->mbc1.mode == 1 ? "RAM" : "ROM"); @@ -1439,7 +1576,7 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg } if (cartridge->has_rumble) { - GB_log(gb, "Cart contains a rumble pak\n"); + GB_log(gb, "Cart contains a Rumble Pak\n"); } if (cartridge->has_rtc) { @@ -1457,9 +1594,9 @@ static bool backtrace(GB_gameboy_t *gb, char *arguments, char *modifiers, const print_usage(gb, command); return true; } - + GB_log(gb, " 1. %s\n", debugger_value_to_string(gb, (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}, true)); - for (unsigned int i = gb->backtrace_size; i--;) { + for (unsigned i = gb->backtrace_size; i--;) { GB_log(gb, "%3d. %s\n", gb->backtrace_size - i + 1, debugger_value_to_string(gb, (value_t){true, gb->backtrace_returns[i].bank, gb->backtrace_returns[i].addr}, true)); } @@ -1476,7 +1613,7 @@ static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu return true; } - GB_log(gb, "Ticks: %lu. (Resetting)\n", gb->debugger_ticks); + GB_log(gb, "Ticks: %llu. (Resetting)\n", (unsigned long long)gb->debugger_ticks); gb->debugger_ticks = 0; return true; @@ -1524,7 +1661,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_is_cgb(gb)? (gb->cgb_mode? "Sprite priority flags" : "Background and Window") : "Background", + GB_log(gb, " %s: %s\n", (gb->cgb_mode? "Sprite 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"); @@ -1532,7 +1669,7 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg GB_log(gb, " Background and Window Tileset: %s\n", (gb->io_registers[GB_IO_LCDC] & 16)? "$8000" : "$8800"); GB_log(gb, " Window: %s\n", (gb->io_registers[GB_IO_LCDC] & 32)? "Enabled" : "Disabled"); GB_log(gb, " Window tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & 64)? "$9C00" : "$9800"); - + GB_log(gb, "\nSTAT:\n"); static const char *modes[] = {"Mode 0, H-Blank", "Mode 1, V-Blank", "Mode 2, OAM", "Mode 3, Rendering"}; GB_log(gb, " Current mode: %s\n", modes[gb->io_registers[GB_IO_STAT] & 3]); @@ -1541,9 +1678,9 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg GB_log(gb, " V-Blank interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 16)? "Enabled" : "Disabled"); GB_log(gb, " OAM interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 32)? "Enabled" : "Disabled"); GB_log(gb, " LYC interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 64)? "Enabled" : "Disabled"); - - - + + + GB_log(gb, "\nCurrent line: %d\n", gb->current_line); GB_log(gb, "Current state: "); if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { @@ -1564,9 +1701,191 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg } GB_log(gb, "LY: %d\n", gb->io_registers[GB_IO_LY]); GB_log(gb, "LYC: %d\n", gb->io_registers[GB_IO_LYC]); - GB_log(gb, "Window position: %d, %d\n", (signed) gb->io_registers[GB_IO_WX] - 7 , gb->io_registers[GB_IO_WY]); + GB_log(gb, "Window position: %d, %d\n", (signed) gb->io_registers[GB_IO_WX] - 7, gb->io_registers[GB_IO_WY]); GB_log(gb, "Interrupt line: %s\n", gb->stat_interrupt_line? "On" : "Off"); - + + return true; +} + +static bool apu(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; + } + + + GB_log(gb, "Current state: "); + if (!gb->apu.global_enable) { + GB_log(gb, "Disabled\n"); + } + else { + GB_log(gb, "Enabled\n"); + for (uint8_t channel = 0; channel < GB_N_CHANNELS; channel++) { + GB_log(gb, "CH%u is %s, DAC %s; current sample = 0x%x\n", channel + 1, + gb->apu.is_active[channel] ? "active " : "inactive", + GB_apu_is_DAC_enabled(gb, channel) ? "active " : "inactive", + gb->apu.samples[channel]); + } + } + + GB_log(gb, "SO1 (left output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x07); + if (gb->io_registers[GB_IO_NR51] & 0x0f) { + for (uint8_t channel = 0, mask = 0x01; channel < GB_N_CHANNELS; channel++, mask <<= 1) { + if (gb->io_registers[GB_IO_NR51] & mask) { + GB_log(gb, " CH%u", channel + 1); + } + } + } + else { + GB_log(gb, " no channels"); + } + GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); + + GB_log(gb, "SO2 (right output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x70 >> 4); + if (gb->io_registers[GB_IO_NR51] & 0xf0) { + for (uint8_t channel = 0, mask = 0x10; channel < GB_N_CHANNELS; channel++, mask <<= 1) { + if (gb->io_registers[GB_IO_NR51] & mask) { + GB_log(gb, " CH%u", channel + 1); + } + } + } + else { + GB_log(gb, " no channels"); + } + GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); + + + for (uint8_t channel = GB_SQUARE_1; channel <= GB_SQUARE_2; channel++) { + GB_log(gb, "\nCH%u:\n", channel + 1); + GB_log(gb, " Current volume: %u, current sample length: %u APU ticks (next in %u ticks)\n", + gb->apu.square_channels[channel].current_volume, + (gb->apu.square_channels[channel].sample_length ^ 0x7FF) * 2 + 1, + gb->apu.square_channels[channel].sample_countdown); + + uint8_t nrx2 = gb->io_registers[channel == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; + GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n", + gb->apu.square_channels[channel].volume_countdown, + nrx2 & 8 ? "in" : "de", + nrx2 & 7); + + uint8_t duty = gb->io_registers[channel == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; + 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)" : ""); + + 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); + } + + if (gb->apu.square_channels[channel].length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.square_channels[channel].pulse_length); + } + } + + + 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]); + } + GB_log(gb, "\n"); + GB_log(gb, " Current position: %u\n", gb->apu.wave_channel.current_sample_index); + + GB_log(gb, " Volume %s (right-shifted %u times)\n", + gb->apu.wave_channel.shift > 4? "" : (const char *[]){"100%", "50%", "25%", "", "muted"}[gb->apu.wave_channel.shift], + gb->apu.wave_channel.shift); + + GB_log(gb, " Current sample length: %u APU ticks (next in %u ticks)\n", + gb->apu.wave_channel.sample_length ^ 0x7ff, + gb->apu.wave_channel.sample_countdown); + + if (gb->apu.wave_channel.length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.wave_channel.pulse_length); + } + + + GB_log(gb, "\nCH4:\n"); + GB_log(gb, " Current volume: %u, current sample length: %u APU ticks (next 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_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n", + gb->apu.noise_channel.volume_countdown, + gb->io_registers[GB_IO_NR42] & 8 ? "in" : "de", + gb->io_registers[GB_IO_NR42] & 7); + + GB_log(gb, " LFSR in %u-step mode, current value ", + gb->apu.noise_channel.narrow? 7 : 15); + for (uint16_t lfsr = gb->apu.noise_channel.lfsr, i = 15; i--; lfsr <<= 1) { + GB_log(gb, "%u%s", (lfsr >> 14) & 1, i%4 ? "" : " "); + } + + if (gb->apu.noise_channel.length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.noise_channel.pulse_length); + } + + + GB_log(gb, "\n\nReminder: APU ticks are @ 2 MiHz\n"); + + return true; +} + +static char *wave_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"c", "f", "l"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +static bool wave(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + if (strlen(lstrip(arguments)) || (modifiers && !strchr("fcl", modifiers[0]))) { + print_usage(gb, command); + return true; + } + + uint8_t shift_amount = 1, mask; + if (modifiers) { + switch (modifiers[0]) { + case 'c': + shift_amount = 2; + break; + case 'l': + shift_amount = 8; + break; + } + } + mask = (0xf << (shift_amount - 1)) & 0xf; + + 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]); + } + else { + GB_log(gb, "%c", i%4 == 2 ? '-' : ' '); + } + } + GB_log(gb, "\n"); + } + return true; } @@ -1580,35 +1899,44 @@ 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"}, - {"backtrace", 2, backtrace, "Display the current call stack"}, + {"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 (Experimental)"}, - {"ticks", 2, ticks, "Display the number of CPU ticks since the last time 'ticks' was used"}, + {"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected"}, + {"ticks", 2, ticks, "Displays the number of CPU ticks since the last time 'ticks' was" HELP_NEWLINE + "used"}, {"registers", 1, registers, "Print values of processor registers and other important registers"}, {"cartridge", 2, mbc, "Displays information about the MBC and cartridge"}, {"mbc", 3, }, /* Alias */ + {"apu", 3, apu, "Displays information about the current state of the audio chip"}, + {"wave", 3, wave, "Prints a visual representation of the wave RAM." HELP_NEWLINE + "Modifiers can be used for a (f)ull print (the default)," HELP_NEWLINE + "a more (c)ompact one, or a one-(l)iner", "", "(f|c|l)", .modifiers_completer = wave_completer}, {"lcd", 3, lcd, "Displays information about the current state of the LCD controller"}, {"palettes", 3, palettes, "Displays the current CGB palettes"}, + {"softbreak", 2, softbreak, "Enables or disables software breakpoints", "(on|off)", .argument_completer = on_off_completer}, {"breakpoint", 1, breakpoint, "Add a new breakpoint at the specified address/expression" HELP_NEWLINE "Can also modify the condition of existing breakpoints." HELP_NEWLINE "If the j modifier is used, the breakpoint will occur just before" HELP_NEWLINE "jumping to the target.", - "[ if ]", "(j)"}, - {"delete", 2, delete, "Delete a breakpoint by its address, or all breakpoints", "[]"}, + "[ if ]", "j", + .argument_completer = symbol_completer, .modifiers_completer = j_completer}, + {"delete", 2, delete, "Delete a breakpoint by its address, or all breakpoints", "[]", .argument_completer = symbol_completer}, {"watch", 1, watch, "Add a new watchpoint at the specified address/expression." HELP_NEWLINE "Can also modify the condition and type of existing watchpoints." HELP_NEWLINE "Default watchpoint type is write-only.", - "[ if ]", "(r|w|rw)"}, - {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[]"}, + "[ if ]", "(r|w|rw)", + .argument_completer = symbol_completer, .modifiers_completer = rw_completer + }, + {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[]", .argument_completer = symbol_completer}, {"list", 1, list, "List all set breakpoints and watchpoints"}, {"print", 1, print, "Evaluate and print an expression" HELP_NEWLINE "Use modifier to format as an address (a, default) or as a number in" HELP_NEWLINE "decimal (d), hexadecimal (x), octal (o) or binary (b).", - "", "format"}, + "", "format", .argument_completer = symbol_completer, .modifiers_completer = format_completer}, {"eval", 2, }, /* Alias */ - {"examine", 2, examine, "Examine values at address", "", "count"}, + {"examine", 2, examine, "Examine values at address", "", "count", .argument_completer = symbol_completer}, {"x", 1, }, /* Alias */ - {"disassemble", 1, disassemble, "Disassemble instructions at address", "", "count"}, + {"disassemble", 1, disassemble, "Disassemble instructions at address", "", "count", .argument_completer = symbol_completer}, {"help", 1, help, "List available commands or show help for the specified command", "[]"}, @@ -1635,7 +1963,7 @@ static const debugger_command_t *find_command(const char *string) static void print_command_shortcut(GB_gameboy_t *gb, const debugger_command_t *command) { GB_attributed_log(gb, GB_LOG_BOLD | GB_LOG_UNDERLINE, "%.*s", command->min_length, command->command); - GB_attributed_log(gb, GB_LOG_BOLD , "%s", command->command + command->min_length); + GB_attributed_log(gb, GB_LOG_BOLD, "%s", command->command + command->min_length); } static void print_command_description(GB_gameboy_t *gb, const debugger_command_t *command) @@ -1755,7 +2083,7 @@ static bool _GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, value_t addr, u } bool error; bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition, - (unsigned int)strlen(gb->watchpoints[index].condition), &error, &addr.value, &value).value; + (unsigned)strlen(gb->watchpoints[index].condition), &error, &addr.value, &value).value; if (error) { /* Should never happen */ GB_log(gb, "An internal error has occured\n"); @@ -1800,7 +2128,7 @@ static bool _GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, value_t addr) } bool error; bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition, - (unsigned int)strlen(gb->watchpoints[index].condition), &error, &addr.value, NULL).value; + (unsigned)strlen(gb->watchpoints[index].condition), &error, &addr.value, NULL).value; if (error) { /* Should never happen */ GB_log(gb, "An internal error has occured\n"); @@ -1864,6 +2192,63 @@ 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; + char *arguments = strchr(input, ' '); + if (arguments) { + /* Actually "split" the string. */ + arguments[0] = 0; + arguments++; + } + + char *modifiers = strchr(command_string, '/'); + if (modifiers) { + /* Actually "split" the string. */ + modifiers[0] = 0; + modifiers++; + } + + const debugger_command_t *command = find_command(command_string); + if (command && command->implementation == help && arguments) { + command_string = arguments; + arguments = NULL; + } + + /* No commands and no modifiers, complete the command */ + if (!arguments && !modifiers) { + size_t length = strlen(command_string); + if (*context >= sizeof(commands) / sizeof(commands[0])) { + return NULL; + } + for (const debugger_command_t *command = &commands[*context]; command->command; command++) { + (*context)++; + if (memcmp(command->command, command_string, length) == 0) { /* Is a substring? */ + return strdup(command->command + length); + } + } + return NULL; + } + + if (command) { + if (arguments) { + if (command->argument_completer) { + return command->argument_completer(gb, arguments, context); + } + return NULL; + } + + if (modifiers) { + if (command->modifiers_completer) { + return command->modifiers_completer(gb, modifiers, context); + } + return NULL; + } + } + return NULL; +} + typedef enum { JUMP_TO_NONE, JUMP_TO_BREAK, @@ -1875,9 +2260,9 @@ 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; - + char *input = NULL; - if (gb->debug_next_command && gb->debug_call_depth <= 0) { + if (gb->debug_next_command && gb->debug_call_depth <= 0 && !gb->halted) { gb->debug_stopped = true; } if (gb->debug_fin_command && gb->debug_call_depth == -1) { @@ -1895,11 +2280,11 @@ next_command: GB_log(gb, "Breakpoint: PC = %s\n", value_to_string(gb, gb->pc, true)); GB_cpu_disassemble(gb, gb->pc, 5); } - + if (gb->breakpoints && !gb->debug_stopped) { uint16_t address = 0; jump_to_return_t jump_to_result = test_jump_to_breakpoints(gb, &address); - + bool should_delete_state = true; if (gb->nontrivial_jump_state && should_break(gb, gb->pc, true)) { if (gb->non_trivial_jump_breakpoint_occured) { @@ -1930,7 +2315,7 @@ next_command: else { gb->non_trivial_jump_breakpoint_occured = false; } - + if (should_delete_state) { if (gb->nontrivial_jump_state) { free(gb->nontrivial_jump_state); @@ -1969,6 +2354,19 @@ void GB_debugger_handle_async_commands(GB_gameboy_t *gb) } } +void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol) +{ + bank &= 0x1FF; + + if (!gb->bank_symbols[bank]) { + gb->bank_symbols[bank] = GB_map_alloc(); + } + GB_bank_symbol_t *allocated_symbol = GB_map_add_symbol(gb->bank_symbols[bank], address, symbol); + if (allocated_symbol) { + GB_reversed_map_add_symbol(&gb->reversed_symbol_map, bank, allocated_symbol); + } +} + void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path) { FILE *f = fopen(path, "r"); @@ -1987,18 +2385,11 @@ void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path) } if (length == 0) continue; - unsigned int bank, address; + unsigned bank, address; char symbol[length]; - if (sscanf(line, "%02x:%04x %s", &bank, &address, symbol) == 3) { - bank &= 0x1FF; - if (!gb->bank_symbols[bank]) { - gb->bank_symbols[bank] = GB_map_alloc(); - } - GB_bank_symbol_t *allocated_symbol = GB_map_add_symbol(gb->bank_symbols[bank], address, symbol); - if (allocated_symbol) { - GB_reversed_map_add_symbol(&gb->reversed_symbol_map, bank, allocated_symbol); - } + if (sscanf(line, "%x:%x %s", &bank, &address, symbol) == 3) { + GB_debugger_add_symbol(gb, bank, address, symbol); } } free(line); @@ -2007,13 +2398,13 @@ void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path) void GB_debugger_clear_symbols(GB_gameboy_t *gb) { - for (int i = sizeof(gb->bank_symbols) / sizeof(gb->bank_symbols[0]); i--;) { + for (unsigned i = sizeof(gb->bank_symbols) / sizeof(gb->bank_symbols[0]); i--;) { if (gb->bank_symbols[i]) { GB_map_free(gb->bank_symbols[i]); gb->bank_symbols[i] = 0; } } - for (int i = sizeof(gb->reversed_symbol_map.buckets) / sizeof(gb->reversed_symbol_map.buckets[0]); i--;) { + for (unsigned i = sizeof(gb->reversed_symbol_map.buckets) / sizeof(gb->reversed_symbol_map.buckets[0]); i--;) { while (gb->reversed_symbol_map.buckets[i]) { GB_symbol_t *next = gb->reversed_symbol_map.buckets[i]->next; free(gb->reversed_symbol_map.buckets[i]); @@ -2077,17 +2468,17 @@ static bool is_in_trivial_memory(uint16_t addr) if (addr < 0x8000) { return true; } - + /* HRAM */ if (addr >= 0xFF80 && addr < 0xFFFF) { return true; } - + /* RAM */ if (addr >= 0xC000 && addr < 0xE000) { return true; } - + return false; } @@ -2125,7 +2516,7 @@ static bool condition_code(GB_gameboy_t *gb, uint8_t opcode) case 3: return (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); } - + return false; } @@ -2134,7 +2525,7 @@ static uint16_t jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode) if (!condition_code(gb, opcode)) { return gb->pc + 2; } - + return gb->pc + 2 + (int8_t)GB_read_memory(gb, gb->pc + 1); } @@ -2221,12 +2612,12 @@ static GB_opcode_address_getter_t *opcodes[256] = { static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *address) { 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)) { return JUMP_TO_NONTRIVIAL; } - + /* Interrupts */ if (gb->ime) { for (unsigned i = 0; i < 5; i++) { @@ -2240,38 +2631,38 @@ static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *add } } } - + uint16_t n_watchpoints = gb->n_watchpoints; gb->n_watchpoints = 0; - + uint8_t opcode = GB_read_memory(gb, gb->pc); - + if (opcode == 0x76) { gb->n_watchpoints = n_watchpoints; if (gb->ime) { /* Already handled in above */ return JUMP_TO_NONE; } - + if (gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F) { return JUMP_TO_NONTRIVIAL; /* HALT bug could occur */ } - + return JUMP_TO_NONE; } - + GB_opcode_address_getter_t *getter = opcodes[opcode]; if (!getter) { gb->n_watchpoints = n_watchpoints; return JUMP_TO_NONE; } - + uint16_t new_pc = getter(gb, opcode); - + gb->n_watchpoints = n_watchpoints; - + if (address) { *address = new_pc; } - + return should_break(gb, new_pc, true) ? JUMP_TO_BREAK : JUMP_TO_NONE; } diff --git a/Core/debugger.h b/Core/debugger.h index 2906ad9..0678b30 100644 --- a/Core/debugger.h +++ b/Core/debugger.h @@ -7,13 +7,15 @@ #ifdef GB_INTERNAL -#ifdef DISABLE_DEBUGGER +#ifdef GB_DISABLE_DEBUGGER #define GB_debugger_run(gb) (void)0 #define GB_debugger_handle_async_commands(gb) (void)0 #define GB_debugger_ret_hook(gb) (void)0 #define GB_debugger_call_hook(gb, addr) (void)addr #define GB_debugger_test_write_watchpoint(gb, addr, value) ((void)addr, (void)value) #define GB_debugger_test_read_watchpoint(gb, addr) (void)addr +#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); @@ -22,7 +24,8 @@ 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); -#endif /* DISABLE_DEBUGGER */ +void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol); +#endif /* GB_DISABLE_DEBUGGER */ #endif #ifdef GB_INTERNAL @@ -31,7 +34,7 @@ bool /* Returns true if debugger waits for more commands. Not relevant for non-G void #endif GB_debugger_execute_command(GB_gameboy_t *gb, char *input); /* Destroys input. */ - +char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *context); /* Destroys input, result requires free */ void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path); const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr); diff --git a/Core/display.c b/Core/display.c index 4989cca..2eb8c42 100644 --- a/Core/display.c +++ b/Core/display.c @@ -27,7 +27,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) { - #pragma unroll + UNROLL for (unsigned i = 8; i--;) { fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { (lower >> 7) | ((upper >> 7) << 1), @@ -43,7 +43,7 @@ static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint } } else { - #pragma unroll + UNROLL for (unsigned i = 8; i--;) { fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { (lower & 1) | ((upper & 1) << 1), @@ -70,7 +70,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; - #pragma unroll + UNROLL 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)]; @@ -99,6 +99,8 @@ static void fifo_overlay_object_row(GB_fifo_t *fifo, uint8_t lower, uint8_t uppe #define LINE_LENGTH (456) #define LINES (144) #define WIDTH (160) +#define BORDERED_WIDTH 256 +#define BORDERED_HEIGHT 224 #define FRAME_LENGTH (LCDC_PERIOD) #define VIRTUAL_LINES (FRAME_LENGTH / LINE_LENGTH) // = 154 @@ -109,22 +111,12 @@ typedef struct __attribute__((packed)) { uint8_t flags; } GB_object_t; -static bool window_enabled(GB_gameboy_t *gb) -{ - if ((gb->io_registers[GB_IO_LCDC] & 0x1) == 0) { - if (!gb->cgb_mode && GB_is_cgb(gb)) { - return false; - } - } - return (gb->io_registers[GB_IO_LCDC] & 0x20) && gb->io_registers[GB_IO_WX] < 167; -} - static void display_vblank(GB_gameboy_t *gb) { gb->vblank_just_occured = true; - /* TODO: Slow in trubo mode! */ - if (GB_is_sgb(gb)) { + /* TODO: Slow in turbo mode! */ + if (GB_is_hle_sgb(gb)) { GB_sgb_render(gb); } @@ -134,25 +126,85 @@ static void display_vblank(GB_gameboy_t *gb) } } - if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || gb->stopped) || gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON)) { + 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)) { /* LCD is off, set screen to white or black (if LCD is on in stop mode) */ - if (gb->sgb) { - uint8_t color = (gb->io_registers[GB_IO_LCDC] & 0x80) && gb->stopped ? 0x3 : 0x0; - for (unsigned i = 0; i < WIDTH * LINES; i++) { - gb->sgb->screen_buffer[i] = color; + if (!GB_is_sgb(gb)) { + uint32_t color = 0; + if (GB_is_cgb(gb)) { + color = GB_convert_rgb15(gb, 0x7FFF, false); } - } - else { - uint32_t color = (gb->io_registers[GB_IO_LCDC] & 0x80) && gb->stopped ? - gb->rgb_encode_callback(gb, 0, 0, 0) : - gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF); - for (unsigned i = 0; i < WIDTH * LINES; i++) { - gb ->screen[i] = color; + else { + color = is_ppu_stopped ? + gb->background_palettes_rgb[0] : + gb->background_palettes_rgb[4]; + } + if (gb->border_mode == GB_BORDER_ALWAYS) { + for (unsigned y = 0; y < LINES; y++) { + for (unsigned x = 0; x < WIDTH; x++) { + gb ->screen[x + y * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH] = color; + } + } + } + else { + for (unsigned i = 0; i < WIDTH * LINES; i++) { + gb ->screen[i] = color; + } } } } + + if (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[] = { + 0x2095, 0x5129, 0x1EAF, 0x1EBA, 0x4648, + 0x30DA, 0x69AD, 0x2B57, 0x2B5D, 0x632C, + 0x1050, 0x3C84, 0x0E07, 0x0E18, 0x2964, + }; + unsigned index = gb->rom? gb->rom[0x14e] % 5 : 0; + gb->borrowed_border.palette[0] = colors[index]; + gb->borrowed_border.palette[10] = colors[5 + index]; + gb->borrowed_border.palette[14] = colors[10 + index]; - gb->vblank_callback(gb); + } + + for (unsigned i = 0; i < 16 * 4; i++) { + border_colors[i] = GB_convert_rgb15(gb, gb->borrowed_border.palette[i], true); + } + + for (unsigned tile_y = 0; tile_y < 28; tile_y++) { + for (unsigned tile_x = 0; tile_x < 32; tile_x++) { + 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; + uint8_t palette = (tile >> 10) & 3; + for (unsigned y = 0; y < 8; y++) { + 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; + uint32_t *output = gb->screen + tile_x * 8 + x + (tile_y * 8 + y) * 256; + if (color == 0) { + *output = border_colors[0]; + } + else { + *output = border_colors[color + palette * 16]; + } + } + } + } + } + } + GB_handle_rumble(gb); + + if (gb->vblank_callback) { + gb->vblank_callback(gb); + } GB_timing_sync(gb); } @@ -163,29 +215,68 @@ static inline uint8_t scale_channel(uint8_t x) static inline uint8_t scale_channel_with_curve(uint8_t x) { - return (uint8_t[]){0,2,4,7,12,18,25,34,42,52,62,73,85,97,109,121,134,146,158,170,182,193,203,213,221,230,237,243,248,251,253,255,}[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]; } -uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color) +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]; +} + +static inline uint8_t scale_channel_with_curve_sgb(uint8_t x) +{ + return (uint8_t[]){0,2,5,9,15,20,27,34,42,50,58,67,76,85,94,104,114,123,133,143,153,163,173,182,192,202,211,220,229,238,247,255}[x]; +} + + +uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border) { uint8_t r = (color) & 0x1F; uint8_t g = (color >> 5) & 0x1F; uint8_t b = (color >> 10) & 0x1F; - if (gb->color_correction_mode == GB_COLOR_CORRECTION_DISABLED) { + if (gb->color_correction_mode == GB_COLOR_CORRECTION_DISABLED || (for_border && !gb->has_sgb_border)) { r = scale_channel(r); g = scale_channel(g); b = scale_channel(b); } else { - r = scale_channel_with_curve(r); - g = scale_channel_with_curve(g); - b = scale_channel_with_curve(b); + 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); + b = agb? scale_channel_with_curve_agb(b) : scale_channel_with_curve(b); if (gb->color_correction_mode != GB_COLOR_CORRECTION_CORRECT_CURVES) { - uint8_t new_g = (g * 3 + b) / 4; - uint8_t new_r = r, new_b = b; - if (gb->color_correction_mode == GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS) { + uint8_t new_r, new_g, new_b; + if (agb) { + new_g = (g * 6 + b * 1) / 7; + } + else { + new_g = (g * 3 + b) / 4; + } + new_r = r; + new_b = b; + if (gb->color_correction_mode == GB_COLOR_CORRECTION_REDUCE_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 * (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_PRESERVE_BRIGHTNESS) { uint8_t old_max = MAX(r, MAX(g, b)); uint8_t new_max = MAX(new_r, MAX(new_g, new_b)); @@ -201,7 +292,7 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color) if (new_min != 0xff) { new_r = 0xff - (0xff - new_r) * (0xff - old_min) / (0xff - new_min); new_g = 0xff - (0xff - new_g) * (0xff - old_min) / (0xff - new_min); - new_b = 0xff - (0xff - new_b) * (0xff - old_min) / (0xff - new_min);; + new_b = 0xff - (0xff - new_b) * (0xff - old_min) / (0xff - new_min); } } r = new_r; @@ -219,7 +310,7 @@ void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index uint8_t *palette_data = background_palette? gb->background_palettes_data : gb->sprite_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); + (background_palette? gb->background_palettes_rgb : gb->sprite_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) @@ -302,13 +393,11 @@ void GB_lcd_off(GB_gameboy_t *gb) gb->vram_write_blocked = false; gb->cgb_palettes_blocked = false; - /* Reset window rendering state */ - gb->wy_diff = 0; - gb->window_disabled_while_active = false; gb->current_line = 0; gb->ly_for_comparison = 0; gb->accessed_oam_row = -1; + gb->wy_triggered = false; } static void add_object_from_index(GB_gameboy_t *gb, unsigned index) @@ -319,6 +408,10 @@ static void add_object_from_index(GB_gameboy_t *gb, unsigned index) if (gb->dma_steps_left && (gb->dma_cycles >= 0 || gb->is_dma_restarting)) { return; } + + if (gb->oam_ppu_blocked) { + return; + } /* This reverse sorts the visible objects by location and priority */ GB_object_t *objects = (GB_object_t *) &gb->oam; @@ -344,30 +437,30 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) bool draw_oam = false; bool bg_enabled = true, bg_priority = false; - if (!gb->bg_fifo_paused) { + if (fifo_size(&gb->bg_fifo)) { fifo_item = fifo_pop(&gb->bg_fifo); bg_priority = fifo_item->bg_priority; - } - - if (!gb->oam_fifo_paused && fifo_size(&gb->oam_fifo)) { - oam_fifo_item = fifo_pop(&gb->oam_fifo); - /* Todo: Verify access timings */ - if (oam_fifo_item->pixel && (gb->io_registers[GB_IO_LCDC] & 2)) { - draw_oam = true; - bg_priority |= oam_fifo_item->bg_priority; + + 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)) { + draw_oam = true; + bg_priority |= oam_fifo_item->bg_priority; + } } } + + if (!fifo_item) return; + /* Drop pixels for scrollings */ - if (gb->position_in_line >= 160 || gb->disable_rendering) { + if (gb->position_in_line >= 160 || (gb->disable_rendering && !gb->sgb)) { gb->position_in_line++; return; } - if (gb->bg_fifo_paused) return; /* Mixing */ - /* Todo: Verify access timings */ if ((gb->io_registers[GB_IO_LCDC] & 0x1) == 0) { if (gb->cgb_mode) { bg_priority = false; @@ -376,8 +469,16 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) bg_enabled = false; } } - if (!GB_is_cgb(gb) && gb->in_window) { - bg_enabled = true; + + uint8_t icd_pixel = 0; + uint32_t *dest = NULL; + if (!gb->sgb) { + if (gb->border_mode != GB_BORDER_ALWAYS) { + dest = gb->screen + gb->lcd_x + gb->current_line * WIDTH; + } + else { + dest = gb->screen + gb->lcd_x + gb->current_line * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH; + } } { @@ -390,11 +491,19 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) } if (gb->sgb) { if (gb->current_lcd_line < LINES) { - gb->sgb->screen_buffer[gb->position_in_line + gb->current_lcd_line * WIDTH] = pixel; + gb->sgb->screen_buffer[gb->lcd_x + gb->current_lcd_line * WIDTH] = gb->stopped? 0 : pixel; } } + else if (gb->model & GB_MODEL_NO_SFC_BIT) { + if (gb->icd_pixel_callback) { + icd_pixel = pixel; + } + } + else if (gb->cgb_palettes_ppu_blocked) { + *dest = gb->rgb_encode_callback(gb, 0, 0, 0); + } else { - gb->screen[gb->position_in_line + gb->current_line * WIDTH] = gb->background_palettes_rgb[fifo_item->palette * 4 + pixel]; + *dest = gb->background_palettes_rgb[fifo_item->palette * 4 + pixel]; } } @@ -406,15 +515,32 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) } if (gb->sgb) { if (gb->current_lcd_line < LINES) { - gb->sgb->screen_buffer[gb->position_in_line + gb->current_lcd_line * WIDTH] = pixel; + gb->sgb->screen_buffer[gb->lcd_x + gb->current_lcd_line * WIDTH] = gb->stopped? 0 : pixel; } } + 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 { - gb->screen[gb->position_in_line + gb->current_line * WIDTH] = gb->sprite_palettes_rgb[oam_fifo_item->palette * 4 + pixel]; + *dest = gb->sprite_palettes_rgb[oam_fifo_item->palette * 4 + pixel]; + } + } + + if (gb->model & GB_MODEL_NO_SFC_BIT) { + if (gb->icd_pixel_callback) { + gb->icd_pixel_callback(gb, icd_pixel); } } gb->position_in_line++; + gb->lcd_x++; + gb->window_is_being_fetched = false; } /* All verified CGB timings are based on CGB CPU E. CGB CPUs >= D are known to have @@ -424,7 +550,7 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) static inline uint8_t fetcher_y(GB_gameboy_t *gb) { - return gb->current_line + (gb->in_window? - gb->io_registers[GB_IO_WY] - gb->wy_diff : gb->io_registers[GB_IO_SCY]); + return gb->wx_triggered? gb->window_y : gb->current_line + gb->io_registers[GB_IO_SCY]; } static void advance_fetcher_state_machine(GB_gameboy_t *gb) @@ -444,36 +570,52 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) GB_FETCHER_GET_TILE_DATA_LOWER, GB_FETCHER_SLEEP, GB_FETCHER_GET_TILE_DATA_HIGH, - GB_FETCHER_SLEEP, + GB_FETCHER_PUSH, GB_FETCHER_PUSH, }; - - switch (fetcher_state_machine[gb->fetcher_state]) { + switch (fetcher_state_machine[gb->fetcher_state & 7]) { case GB_FETCHER_GET_TILE: { uint16_t map = 0x1800; + if (!(gb->io_registers[GB_IO_LCDC] & 0x20)) { + gb->wx_triggered = false; + gb->wx166_glitch = false; + } + /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ - if (gb->io_registers[GB_IO_LCDC] & 0x08 && !gb->in_window) { + if (gb->io_registers[GB_IO_LCDC] & 0x08 && !gb->wx_triggered) { map = 0x1C00; } - else if (gb->io_registers[GB_IO_LCDC] & 0x40 && gb->in_window) { + else if (gb->io_registers[GB_IO_LCDC] & 0x40 && gb->wx_triggered) { map = 0x1C00; } /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ uint8_t y = fetcher_y(gb); + uint8_t x = 0; + if (gb->wx_triggered) { + x = gb->window_tile_x; + } + else { + x = ((gb->io_registers[GB_IO_SCX] / 8) + gb->fetcher_x) & 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 */ gb->fetcher_y = y; } - gb->current_tile = gb->vram[map + gb->fetcher_x + y / 8 * 32]; + gb->last_tile_index_address = map + x + y / 8 * 32; + gb->current_tile = gb->vram[gb->last_tile_index_address]; + if (gb->vram_ppu_blocked) { + gb->current_tile = 0xFF; + } if (GB_is_cgb(gb)) { /* The CGB actually accesses both the tile index AND the attributes in the same T-cycle. - This probably means the CGB has a 16-bit data bus for the VRAM. */ - gb->current_tile_attributes = gb->vram[map + gb->fetcher_x + y / 8 * 32 + 0x2000]; + This probably means the CGB has a 16-bit data bus for the VRAM. */ + gb->current_tile_attributes = gb->vram[gb->last_tile_index_address + 0x2000]; + if (gb->vram_ppu_blocked) { + gb->current_tile_attributes = 0xFF; + } } - gb->fetcher_x++; - gb->fetcher_x &= 0x1f; } gb->fetcher_state++; break; @@ -497,7 +639,10 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) y_flip = 0x7; } gb->current_tile_data[0] = - gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; + gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; + if (gb->vram_ppu_blocked) { + gb->current_tile_data[0] = 0xFF; + } } gb->fetcher_state++; break; @@ -523,19 +668,34 @@ 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[tile_address + ((y & 7) ^ y_flip) * 2 + 1]; + gb->vram[gb->last_tile_data_address]; + if (gb->vram_ppu_blocked) { + gb->current_tile_data[1] = 0xFF; + } + } + if (gb->wx_triggered) { + gb->window_tile_x++; + gb->window_tile_x &= 0x1f; } - gb->fetcher_state++; - break; + // 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++; + } if (fifo_size(&gb->bg_fifo) > 0) break; + fifo_push_bg_row(&gb->bg_fifo, gb->current_tile_data[0], gb->current_tile_data[1], gb->current_tile_attributes & 7, gb->current_tile_attributes & 0x80, gb->current_tile_attributes & 0x20); - gb->bg_fifo_paused = false; - gb->oam_fifo_paused = false; - gb->fetcher_state++; + gb->fetcher_state = 0; } break; @@ -545,8 +705,30 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) } break; } +} + +static uint16_t get_object_line_address(GB_gameboy_t *gb, const GB_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}; + object = &blocked; + } - gb->fetcher_state &= 7; + bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; /* Todo: Which T-cycle actually reads this? */ + uint8_t tile_y = (gb->current_line - object->y) & (height_16? 0xF : 7); + + if (object->flags & 0x40) { /* Flip Y */ + tile_y ^= height_16? 0xF : 7; + } + + /* Todo: I'm not 100% sure an access to OAM can't trigger the OAM bug while we're accessing this */ + uint16_t line_address = (height_16? object->tile & 0xFE : object->tile) * 0x10 + tile_y * 2; + + if (gb->cgb_mode && (object->flags & 0x8)) { /* Use VRAM bank 2 */ + line_address += 0x2000; + } + return line_address; } /* @@ -555,6 +737,15 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) */ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) { + /* 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); + } + return; + } GB_object_t *objects = (GB_object_t *) &gb->oam; GB_STATE_MACHINE(gb, display, cycles, 2) { @@ -595,23 +786,38 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_STATE(gb, display, 36); GB_STATE(gb, display, 37); GB_STATE(gb, display, 38); - + GB_STATE(gb, display, 39); + GB_STATE(gb, display, 40); + GB_STATE(gb, display, 41); + GB_STATE(gb, display, 42); } if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { while (true) { GB_SLEEP(gb, display, 1, LCDC_PERIOD); display_vblank(gb); + gb->cgb_repeated_a_frame = true; } return; } + gb->is_odd_frame = false; + if (!GB_is_cgb(gb)) { GB_SLEEP(gb, display, 23, 1); } /* 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->ly_for_comparison = 0; gb->io_registers[GB_IO_STAT] &= ~3; gb->mode_for_interrupt = -1; @@ -648,17 +854,25 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_SLEEP(gb, display, 37, 2); gb->cgb_palettes_blocked = true; - gb->cycles_for_line += 2; - GB_SLEEP(gb, display, 38, 2); + gb->cycles_for_line += (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C)? 2 : 3; + GB_SLEEP(gb, display, 38, (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C)? 2 : 3); gb->vram_read_blocked = true; gb->vram_write_blocked = true; + gb->wx_triggered = false; + gb->wx166_glitch = false; goto mode_3_start; - while (true) { /* Lines 0 - 143 */ + gb->window_y = -1; for (; gb->current_line < LINES; gb->current_line++) { + /* Todo: verify timings */ + if ((gb->io_registers[GB_IO_WY] == gb->current_line || + (gb->current_line != 0 && gb->io_registers[GB_IO_WY] == gb->current_line - 1))) { + gb->wy_triggered = true; + } + gb->oam_write_blocked = GB_is_cgb(gb) && !gb->cgb_double_speed; gb->accessed_oam_row = 0; @@ -696,7 +910,7 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) 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); - /* The CGB does not care about the accessed OAM row as there's no OAM bug*/ + /* The CGB does not care about the accessed OAM row as there's no OAM bug */ } GB_SLEEP(gb, display, 8, 2); if (!GB_is_cgb(gb)) { @@ -744,20 +958,80 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) fifo_push_bg_row(&gb->bg_fifo, 0, 0, 0, false, false); /* Todo: find out actual access time of SCX */ gb->position_in_line = - (gb->io_registers[GB_IO_SCX] & 7) - 8; - gb->current_lcd_line++; // Todo: unverified timing - if (gb->current_lcd_line == LINES) { - display_vblank(gb); - } - gb->fetcher_x = ((gb->io_registers[GB_IO_SCX]) / 8) & 0x1f; + gb->lcd_x = 0; + + gb->fetcher_x = 0; gb->extra_penalty_for_sprite_at_0 = (gb->io_registers[GB_IO_SCX] & 7); /* The actual rendering cycle */ gb->fetcher_state = 0; - gb->bg_fifo_paused = false; - gb->oam_fifo_paused = false; - gb->in_window = false; while (true) { + /* Handle window */ + /* TODO: It appears that WX checks if the window begins *next* pixel, not *this* pixel. For this reason, + WX=167 has no effect at all (It checks if the PPU X position is 161, which never happens) and WX=166 + has weird artifacts (It appears to activate the window during HBlank, as PPU X is temporarily 160 at + that point. The code should be updated to represent this, and this will fix the time travel hack in + WX's access conflict code. */ + + if (!gb->wx_triggered && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20)) { + bool should_activate_window = false; + if (gb->io_registers[GB_IO_WX] == 0) { + static const uint8_t scx_to_wx0_comparisons[] = {-7, -9, -10, -11, -12, -13, -14, -14}; + if (gb->position_in_line == scx_to_wx0_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { + should_activate_window = true; + } + } + else if (gb->wx166_glitch) { + static const uint8_t scx_to_wx166_comparisons[] = {-8, -9, -10, -11, -12, -13, -14, -15}; + if (gb->position_in_line == scx_to_wx166_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { + should_activate_window = true; + } + } + else if (gb->io_registers[GB_IO_WX] < 166 + GB_is_cgb(gb)) { + if (gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7)) { + should_activate_window = true; + } + else if (gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 6) && !gb->wx_just_changed) { + should_activate_window = true; + /* LCD-PPU horizontal desync! It only appears to happen on DMGs, but not all of them. + This doesn't seem to be CPU revision dependent, but most revisions */ + if ((gb->model & GB_MODEL_FAMILY_MASK) == GB_MODEL_DMG_FAMILY && !GB_is_sgb(gb)) { + if (gb->lcd_x > 0) { + gb->lcd_x--; + } + } + } + } + + if (should_activate_window) { + gb->window_y++; + /* TODO: Verify fetcher access timings in this case */ + if (gb->io_registers[GB_IO_WX] == 0 && (gb->io_registers[GB_IO_SCX] & 7)) { + gb->cycles_for_line++; + GB_SLEEP(gb, display, 42, 1); + } + gb->wx_triggered = true; + gb->window_tile_x = 0; + fifo_clear(&gb->bg_fifo); + gb->fetcher_state = 0; + gb->window_is_being_fetched = true; + } + else if (!GB_is_cgb(gb) && gb->io_registers[GB_IO_WX] == 166 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7)) { + gb->window_y++; + } + } + + /* TODO: What happens when WX=0? */ + if (!GB_is_cgb(gb) && gb->wx_triggered && !gb->window_is_being_fetched && + gb->fetcher_state == 0 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7) ) { + // Insert a pixel right at the FIFO's end + gb->bg_fifo.read_end--; + gb->bg_fifo.read_end &= GB_FIFO_LENGTH - 1; + gb->bg_fifo.fifo[gb->bg_fifo.read_end] = (GB_fifo_item_t){0,}; + gb->window_is_being_fetched = false; + } + /* Handle objects */ /* When the sprite 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. */ @@ -767,14 +1041,19 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) gb->obj_comparators[gb->n_visible_objs - 1] < (uint8_t)(gb->position_in_line + 8)) { gb->n_visible_objs--; } + + gb->during_object_fetch = true; while (gb->n_visible_objs != 0 && (gb->io_registers[GB_IO_LCDC] & 2 || GB_is_cgb(gb)) && gb->obj_comparators[gb->n_visible_objs - 1] == (uint8_t)(gb->position_in_line + 8)) { - while (gb->fetcher_state < 5) { + while (gb->fetcher_state < 5 || fifo_size(&gb->bg_fifo) == 0) { advance_fetcher_state_machine(gb); gb->cycles_for_line++; GB_SLEEP(gb, display, 27, 1); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } } /* Todo: Measure if penalty occurs before or after waiting for the fetcher. */ @@ -783,56 +1062,63 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) 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; + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } } } - gb->cycles_for_line += 6; - GB_SLEEP(gb, display, 20, 6); - /* TODO: what does the PPU read if DMA is active? */ - GB_object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; + /* TODO: Can this be deleted? { */ + advance_fetcher_state_machine(gb); + gb->cycles_for_line++; + GB_SLEEP(gb, display, 41, 1); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } + /* } */ - bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; /* Todo: Which T-cycle actually reads this? */ - uint8_t tile_y = (gb->current_line - object->y) & (height_16? 0xF : 7); + advance_fetcher_state_machine(gb); - if (object->flags & 0x40) { /* Flip Y */ - tile_y ^= height_16? 0xF : 7; + gb->cycles_for_line += 3; + GB_SLEEP(gb, display, 20, 3); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; } - /* Todo: I'm not 100% sure an access to OAM can't trigger the OAM bug while we're accessing this */ - uint16_t line_address = (height_16? object->tile & 0xFE : object->tile) * 0x10 + tile_y * 2; + gb->object_low_line_address = get_object_line_address(gb, &objects[gb->visible_objs[gb->n_visible_objs - 1]]); - if (gb->cgb_mode && (object->flags & 0x8)) { /* Use VRAM bank 2 */ - line_address += 0x2000; + gb->cycles_for_line++; + GB_SLEEP(gb, display, 39, 1); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; } + gb->during_object_fetch = false; + gb->cycles_for_line++; + GB_SLEEP(gb, display, 40, 1); + + const GB_object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; + + uint16_t line_address = get_object_line_address(gb, object); + uint8_t palette = (object->flags & 0x10) ? 1 : 0; if (gb->cgb_mode) { palette = object->flags & 0x7; } - fifo_overlay_object_row(&gb->oam_fifo, - gb->vram[line_address], - gb->vram[line_address + 1], + gb->vram_ppu_blocked? 0xFF : gb->vram[gb->object_low_line_address], + gb->vram_ppu_blocked? 0xFF : gb->vram[line_address + 1], palette, object->flags & 0x80, - gb->cgb_mode? gb->visible_objs[gb->n_visible_objs - 1] : 0, + gb->object_priority == GB_OBJECT_PRIORITY_INDEX? gb->visible_objs[gb->n_visible_objs - 1] : 0, object->flags & 0x20); gb->n_visible_objs--; } - /* Handle window */ - /* Todo: Timing (Including penalty and access timings) not verified by test ROM */ - if (!gb->in_window && window_enabled(gb) && - gb->current_line >= gb->io_registers[GB_IO_WY] + gb->wy_diff && - (uint8_t)(gb->position_in_line + 7) == gb->io_registers[GB_IO_WX]) { - gb->in_window = true; - fifo_clear(&gb->bg_fifo); - gb->bg_fifo_paused = true; - gb->oam_fifo_paused = true; - gb->fetcher_x = 0; - gb->fetcher_state = 0; - } +abort_fetching_object: + gb->object_fetch_aborted = false; + gb->during_object_fetch = false; render_pixel_if_possible(gb); advance_fetcher_state_machine(gb); @@ -842,6 +1128,29 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_SLEEP(gb, display, 21, 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; + if (gb->border_mode != GB_BORDER_ALWAYS) { + dest = gb->screen + gb->lcd_x + gb->current_line * WIDTH; + } + else { + dest = gb->screen + gb->lcd_x + gb->current_line * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH; + } + *dest = gb->background_palettes_rgb[0]; + gb->lcd_x++; + + } + + /* TODO: Verify timing */ + if (!GB_is_cgb(gb) && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20) && gb->io_registers[GB_IO_WX] == 166) { + gb->wx166_glitch = true; + } + else { + gb->wx166_glitch = false; + } + gb->wx_triggered = false; + if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C) { gb->cycles_for_line++; GB_SLEEP(gb, display, 30, 1); @@ -884,8 +1193,18 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) } GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line); 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); + } + + if (gb->icd_hreset_callback) { + gb->icd_hreset_callback(gb); + } } - + 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; @@ -909,12 +1228,30 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_STAT_update(gb); if (gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) { - display_vblank(gb); - gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + if (GB_is_cgb(gb)) { + GB_timing_sync(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->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + } } else { - gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; - display_vblank(gb); + if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) { + gb->is_odd_frame ^= true; + display_vblank(gb); + } + if (gb->frame_skip_state == GB_FRAMESKIP_FIRST_FRAME_SKIPPED) { + gb->cgb_repeated_a_frame = true; + gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + } + else { + gb->cgb_repeated_a_frame = false; + } } } @@ -947,11 +1284,21 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_SLEEP(gb, display, 17, LINE_LENGTH - 24); - /* Reset window rendering state */ - gb->wy_diff = 0; - gb->window_disabled_while_active = false; gb->current_line = 0; - gb->current_lcd_line = -1; // TODO: not the correct timing + /* 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; + } + + // TODO: not the correct timing + gb->current_lcd_line = 0; + if (gb->icd_vreset_callback) { + gb->icd_vreset_callback(gb); + } } } @@ -1084,7 +1431,7 @@ uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_h GB_object_t *sprite = (GB_object_t *) &gb->oam; uint8_t sprites_in_line = 0; for (uint8_t i = 0; i < 40; i++, sprite++) { - int sprite_y = sprite->y - 16; + signed sprite_y = sprite->y - 16; bool obscured = false; // Is sprite not in this line? if (sprite_y > y || sprite_y + *sprite_height <= y) continue; @@ -1117,7 +1464,7 @@ uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_h } for (unsigned y = 0; y < *sprite_height; y++) { - #pragma unroll + UNROLL 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 ); @@ -1133,30 +1480,8 @@ uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_h return count; } -/* Called when a write might enable or disable the window */ -void GB_window_related_write(GB_gameboy_t *gb, uint8_t addr, uint8_t value) + +bool GB_is_odd_frame(GB_gameboy_t *gb) { - bool before = window_enabled(gb); - gb->io_registers[addr] = value; - bool after = window_enabled(gb); - - if (before != after && gb->current_line < LINES) { - /* Window was disabled or enabled outside of vblank */ - if (gb->current_line >= gb->io_registers[GB_IO_WY]) { - if (after) { - if (!gb->window_disabled_while_active) { - /* Window was turned on for the first time this frame while LY > WY, - should start window in the next line */ - gb->wy_diff = gb->current_line + 1 - gb->io_registers[GB_IO_WY]; - } - else { - gb->wy_diff += gb->current_line; - } - } - else { - gb->wy_diff -= gb->current_line; - gb->window_disabled_while_active = true; - } - } - } + return gb->is_odd_frame; } diff --git a/Core/display.h b/Core/display.h index 9d1d1b4..5bdeba8 100644 --- a/Core/display.h +++ b/Core/display.h @@ -8,9 +8,15 @@ #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_window_related_write(GB_gameboy_t *gb, uint8_t addr, uint8_t value); void GB_STAT_update(GB_gameboy_t *gb); void GB_lcd_off(GB_gameboy_t *gb); + +enum { + GB_OBJECT_PRIORITY_UNDEFINED, // For save state compatibility + GB_OBJECT_PRIORITY_X, + GB_OBJECT_PRIORITY_INDEX, +}; + #endif typedef enum { @@ -44,11 +50,13 @@ typedef enum { GB_COLOR_CORRECTION_CORRECT_CURVES, GB_COLOR_CORRECTION_EMULATE_HARDWARE, GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS, + GB_COLOR_CORRECTION_REDUCE_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); -uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color); +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); +bool GB_is_odd_frame(GB_gameboy_t *gb); #endif /* display_h */ diff --git a/Core/gb.c b/Core/gb.c index 9456f10..1bc2235 100644 --- a/Core/gb.c +++ b/Core/gb.c @@ -9,13 +9,22 @@ #include #include #endif +#include "random.h" #include "gb.h" -#ifdef DISABLE_REWIND + +#ifdef GB_DISABLE_REWIND #define GB_rewind_free(...) #define GB_rewind_push(...) #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; @@ -48,11 +57,14 @@ void GB_log(GB_gameboy_t *gb, const char *fmt, ...) va_end(args); } -#ifndef DISABLE_DEBUGGER +#ifndef GB_DISABLE_DEBUGGER static char *default_input_callback(GB_gameboy_t *gb) { char *expression = NULL; size_t size = 0; + if (gb->debug_stopped) { + printf(">"); + } if (getline(&expression, &size, stdin) == -1) { /* The user doesn't have STDIN or used ^D. We make sure the program keeps running. */ @@ -68,6 +80,12 @@ static char *default_input_callback(GB_gameboy_t *gb) if (expression[length - 1] == '\n') { expression[length - 1] = 0; } + + if (expression[0] == '\x03') { + gb->debug_stopped = true; + free(expression); + return strdup(""); + } return expression; } @@ -90,12 +108,48 @@ static char *default_async_input_callback(GB_gameboy_t *gb) } #endif +static void load_default_border(GB_gameboy_t *gb) +{ + if (gb->has_sgb_border) return; + + #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);\ + }\ + }\ + }\ + } while (false); + + if (gb->model == GB_MODEL_AGB) { + #include "graphics/agb_border.inc" + LOAD_BORDER(); + } + else if (GB_is_cgb(gb)) { + #include "graphics/cgb_border.inc" + LOAD_BORDER(); + } + else { + #include "graphics/dmg_border.inc" + LOAD_BORDER(); + } +} + void GB_init(GB_gameboy_t *gb, GB_model_t model) { memset(gb, 0, sizeof(*gb)); gb->model = model; if (GB_is_cgb(gb)) { - gb->ram = malloc(gb->ram_size = 0x2000 * 8); + gb->ram = malloc(gb->ram_size = 0x1000 * 8); gb->vram = malloc(gb->vram_size = 0x2000 * 2); } else { @@ -103,14 +157,20 @@ void GB_init(GB_gameboy_t *gb, GB_model_t model) gb->vram = malloc(gb->vram_size = 0x2000); } -#ifndef DISABLE_DEBUGGER +#ifndef GB_DISABLE_DEBUGGER gb->input_callback = default_input_callback; gb->async_input_callback = default_async_input_callback; #endif gb->cartridge_type = &GB_cart_defs[0]; // Default cartridge type gb->clock_multiplier = 1.0; + if (model & GB_MODEL_NO_SFC_BIT) { + /* Disable time syncing. Timing should be done by the SFC emulator. */ + gb->turbo = true; + } + GB_reset(gb); + load_default_border(gb); } GB_model_t GB_get_model(GB_gameboy_t *gb) @@ -133,9 +193,6 @@ void GB_free(GB_gameboy_t *gb) if (gb->rom) { free(gb->rom); } - if (gb->apu_output.buffer) { - free(gb->apu_output.buffer); - } if (gb->breakpoints) { free(gb->breakpoints); } @@ -145,10 +202,15 @@ void GB_free(GB_gameboy_t *gb) if (gb->nontrivial_jump_state) { free(gb->nontrivial_jump_state); } -#ifndef DISABLE_DEBUGGER +#ifndef GB_DISABLE_DEBUGGER GB_debugger_clear_symbols(gb); #endif GB_rewind_free(gb); +#ifndef GB_DISABLE_CHEATS + while (gb->cheats) { + GB_remove_cheat(gb, gb->cheats[0]); + } +#endif memset(gb, 0, sizeof(*gb)); } @@ -173,6 +235,46 @@ void GB_load_boot_rom_from_buffer(GB_gameboy_t *gb, const unsigned char *buffer, memcpy(gb->boot_rom, buffer, size); } +void GB_borrow_sgb_border(GB_gameboy_t *gb) +{ + if (GB_is_sgb(gb)) return; + if (gb->border_mode != GB_BORDER_ALWAYS) return; + if (gb->tried_loading_sgb_border) return; + gb->tried_loading_sgb_border = true; + if (gb->rom && gb->rom[0x146] != 3) return; // Not an SGB game, nothing to borrow + if (!gb->boot_rom_load_callback) return; // Can't borrow a border without this callback + GB_gameboy_t sgb; + GB_init(&sgb, GB_MODEL_SGB); + sgb.cartridge_type = gb->cartridge_type; + sgb.rom = gb->rom; + sgb.rom_size = gb->rom_size; + sgb.turbo = true; + sgb.turbo_dont_skip = true; + // sgb.disable_rendering = true; + + /* Load the boot ROM using the existing gb object */ + typeof(gb->boot_rom) boot_rom_backup; + memcpy(boot_rom_backup, gb->boot_rom, sizeof(gb->boot_rom)); + gb->boot_rom_load_callback(gb, GB_BOOT_ROM_SGB); + memcpy(sgb.boot_rom, gb->boot_rom, sizeof(gb->boot_rom)); + memcpy(gb->boot_rom, boot_rom_backup, sizeof(gb->boot_rom)); + sgb.sgb->intro_animation = -1; + + for (unsigned i = 600; i--;) { + GB_run_frame(&sgb); + if (sgb.sgb->border_animation) { + gb->has_sgb_border = true; + memcpy(&gb->borrowed_border, &sgb.sgb->pending_border, sizeof(gb->borrowed_border)); + gb->borrowed_border.palette[0] = sgb.sgb->effective_palettes[0]; + break; + } + } + + sgb.rom = NULL; + sgb.rom_size = 0; + GB_free(&sgb); +} + int GB_load_rom(GB_gameboy_t *gb, const char *path) { FILE *f = fopen(path, "rb"); @@ -188,19 +290,272 @@ 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) { + gb->rom_size = 0x8000; + } fseek(f, 0, SEEK_SET); if (gb->rom) { free(gb->rom); } gb->rom = malloc(gb->rom_size); memset(gb->rom, 0xFF, gb->rom_size); /* Pad with 0xFFs */ - fread(gb->rom, gb->rom_size, 1, f); + fread(gb->rom, 1, gb->rom_size, f); fclose(f); GB_configure_cart(gb); - return 0; } +int GB_load_isx(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + GB_log(gb, "Could not open ISX file: %s.\n", strerror(errno)); + return errno; + } + char magic[4]; +#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 + + fseek(f, extended? 0x20 : 0, SEEK_SET); + + + uint8_t *old_rom = gb->rom; + uint32_t old_size = gb->rom_size; + gb->rom = NULL; + gb->rom_size = 0; + + while (true) { + uint8_t record_type = 0; + if (fread(&record_type, sizeof(record_type), 1, f) != 1) break; + switch (record_type) { + case 0x01: { // Binary + uint16_t bank; + uint16_t address; + uint16_t length; + uint8_t byte; + READ(byte); + bank = byte; + if (byte >= 0x80) { + READ(byte); + bank |= byte << 8; + } + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap16(address); +#endif + address &= 0x3FFF; + + READ(length); +#ifdef GB_BIG_ENDIAN + length = __builtin_bswap16(length); +#endif + + size_t needed_size = bank * 0x4000 + address + length; + if (needed_size > 1024 * 1024 * 32) goto error; + + if (gb->rom_size < needed_size) { + gb->rom = realloc(gb->rom, needed_size); + memset(gb->rom + gb->rom_size, 0, needed_size - gb->rom_size); + gb->rom_size = needed_size; + } + + if (fread(gb->rom + (bank * 0x4000 + address), length, 1, f) != 1) goto error; + + break; + } + + case 0x11: { // Extended Binary + uint32_t address; + uint32_t length; + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap32(address); +#endif + + READ(length); +#ifdef GB_BIG_ENDIAN + length = __builtin_bswap32(length); +#endif + size_t needed_size = address + length; + if (needed_size > 1024 * 1024 * 32) goto error; + + if (gb->rom_size < needed_size) { + gb->rom = realloc(gb->rom, needed_size); + memset(gb->rom + gb->rom_size, 0, needed_size - gb->rom_size); + gb->rom_size = needed_size; + } + + if (fread(gb->rom + address, length, 1, f) != 1) goto error; + + break; + } + + case 0x04: { // Symbol + uint16_t count; + uint8_t length; + char name[257]; + uint8_t flag; + uint16_t bank; + uint16_t address; + uint8_t byte; + READ(count); +#ifdef GB_BIG_ENDIAN + count = __builtin_bswap16(count); +#endif + while (count--) { + READ(length); + if (fread(name, length, 1, f) != 1) goto error; + name[length] = 0; + READ(flag); // unused + + READ(byte); + bank = byte; + if (byte >= 0x80) { + READ(byte); + bank |= byte << 8; + } + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap16(address); +#endif + GB_debugger_add_symbol(gb, bank, address, name); + } + break; + } + + case 0x14: { // Extended Binary + uint16_t count; + uint8_t length; + char name[257]; + uint8_t flag; + uint32_t address; + READ(count); +#ifdef GB_BIG_ENDIAN + count = __builtin_bswap16(count); +#endif + while (count--) { + READ(length); + if (fread(name, length + 1, 1, f) != 1) goto error; + name[length] = 0; + READ(flag); // unused + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap32(address); +#endif + // TODO: How to convert 32-bit addresses to Bank:Address? Needs to tell RAM and ROM apart + } + break; + } + + default: + goto done; + } + } +done:; +#undef READ + if (gb->rom_size == 0) goto error; + + size_t needed_size = (gb->rom_size + 0x3FFF) & ~0x3FFF; /* Round to bank */ + + /* And then round to a power of two */ + while (needed_size & (needed_size - 1)) { + /* I promise this works. */ + needed_size |= needed_size >> 1; + needed_size++; + } + + if (needed_size < 0x8000) { + needed_size = 0x8000; + } + + if (gb->rom_size < needed_size) { + gb->rom = realloc(gb->rom, needed_size); + memset(gb->rom + gb->rom_size, 0, needed_size - gb->rom_size); + gb->rom_size = needed_size; + } + + GB_configure_cart(gb); + + // Fix a common wrong MBC error + if (gb->rom[0x147] == 3) { // MBC1 + RAM + Battery + bool needs_fix = false; + if (gb->rom_size >= 0x21 * 0x4000) { + for (unsigned i = 0x20 * 0x4000; i < 0x21 * 0x4000; i++) { + if (gb->rom[i]) { + needs_fix = true; + break; + } + } + } + if (!needs_fix && gb->rom_size >= 0x41 * 0x4000) { + for (unsigned i = 0x40 * 0x4000; i < 0x41 * 0x4000; i++) { + if (gb->rom[i]) { + needs_fix = true; + break; + } + } + } + if (!needs_fix && gb->rom_size >= 0x61 * 0x4000) { + for (unsigned i = 0x60 * 0x4000; i < 0x61 * 0x4000; i++) { + if (gb->rom[i]) { + needs_fix = true; + break; + } + } + } + if (needs_fix) { + gb->rom[0x147] = 0x10; // MBC3 + RTC + RAM + Battery + GB_configure_cart(gb); + gb->rom[0x147] = 0x3; + GB_log(gb, "ROM claims to use MBC1 but appears to require MBC3 or 5, assuming MBC3.\n"); + } + } + + if (old_rom) { + free(old_rom); + } + + return 0; +error: + GB_log(gb, "Invalid or unsupported ISX file.\n"); + if (gb->rom) { + free(gb->rom); + gb->rom = old_rom; + gb->rom_size = old_size; + } + fclose(f); + return -1; +} + +void GB_load_rom_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size) +{ + gb->rom_size = (size + 0x3fff) & ~0x3fff; + while (gb->rom_size & (gb->rom_size - 1)) { + gb->rom_size |= gb->rom_size >> 1; + gb->rom_size++; + } + if (gb->rom_size == 0) { + gb->rom_size = 0x8000; + } + if (gb->rom) { + free(gb->rom); + } + gb->rom = malloc(gb->rom_size); + memset(gb->rom, 0xff, gb->rom_size); + memcpy(gb->rom, buffer, size); + GB_configure_cart(gb); +} + typedef struct { uint8_t seconds; uint8_t padding1[3]; @@ -214,6 +569,14 @@ typedef struct { uint8_t padding5[3]; } GB_vba_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 union { struct __attribute__((packed)) { GB_rtc_time_t rtc_real; @@ -231,6 +594,75 @@ typedef union { } vba64; } GB_rtc_save_t; +int GB_save_battery_size(GB_gameboy_t *gb) +{ + if (!gb->cartridge_type->has_battery) 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); + } + GB_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->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) { + 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, + }; +#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.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; + rtc_save.vba64.rtc_real.days = gb->rtc_real.days; + rtc_save.vba64.rtc_real.high = gb->rtc_real.high; + rtc_save.vba64.rtc_latched.seconds = gb->rtc_latched.seconds; + rtc_save.vba64.rtc_latched.minutes = gb->rtc_latched.minutes; + 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 + memcpy(buffer + gb->mbc_ram_size, &rtc_save.vba64, sizeof(rtc_save.vba64)); + } + + errno = 0; + return errno; +} + int GB_save_battery(GB_gameboy_t *gb, const char *path) { if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. @@ -245,7 +677,33 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) fclose(f); return EIO; } - if (gb->cartridge_type->has_rtc) { + if (gb->cartridge_type->mbc_type == GB_HUC3) { +#ifdef GB_BIG_ENDIAN + GB_huc3_rtc_time_t rtc_save = { + __builtin_bswap64(gb->last_rtc_second), + __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, + }; +#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); + return EIO; + } + } + else if (gb->cartridge_type->has_rtc) { GB_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; @@ -274,6 +732,110 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) return errno; } +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)); + if (size <= gb->mbc_ram_size) { + goto reset_rtc; + } + + 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 + if (gb->last_rtc_second > time(NULL)) { + /* We must reset RTC here, or it will not advance. */ + goto reset_rtc; + } + return; + } + + GB_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): + memcpy(&gb->rtc_real, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); + memcpy(&gb->rtc_latched, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); + gb->last_rtc_second = rtc_save.sameboy_legacy.last_rtc_second; + break; + + case sizeof(rtc_save.vba32): + gb->rtc_real.seconds = rtc_save.vba32.rtc_real.seconds; + gb->rtc_real.minutes = rtc_save.vba32.rtc_real.minutes; + gb->rtc_real.hours = rtc_save.vba32.rtc_real.hours; + gb->rtc_real.days = rtc_save.vba32.rtc_real.days; + gb->rtc_real.high = rtc_save.vba32.rtc_real.high; + gb->rtc_latched.seconds = rtc_save.vba32.rtc_latched.seconds; + gb->rtc_latched.minutes = rtc_save.vba32.rtc_latched.minutes; + 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 + break; + + case sizeof(rtc_save.vba64): + gb->rtc_real.seconds = rtc_save.vba64.rtc_real.seconds; + gb->rtc_real.minutes = rtc_save.vba64.rtc_real.minutes; + gb->rtc_real.hours = rtc_save.vba64.rtc_real.hours; + gb->rtc_real.days = rtc_save.vba64.rtc_real.days; + gb->rtc_real.high = rtc_save.vba64.rtc_real.high; + gb->rtc_latched.seconds = rtc_save.vba64.rtc_latched.seconds; + gb->rtc_latched.minutes = rtc_save.vba64.rtc_latched.minutes; + 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 + break; + + default: + goto reset_rtc; + } + if (gb->last_rtc_second > time(NULL)) { + /* We must reset RTC here, or it will not advance. */ + goto reset_rtc; + } + + if (gb->last_rtc_second < 852076800) { /* 1/1/97. There weren't any RTC games that time, + so if the value we read is lower it means it wasn't + really RTC data. */ + goto reset_rtc; + } + goto exit; +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; +exit: + return; +} + /* Loading will silently stop if the format is incomplete */ void GB_load_battery(GB_gameboy_t *gb, const char *path) { @@ -285,6 +847,33 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) if (fread(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) { goto reset_rtc; } + + 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 + if (gb->last_rtc_second > time(NULL)) { + /* We must reset RTC here, or it will not advance. */ + goto reset_rtc; + } + return; + } GB_rtc_save_t rtc_save; switch (fread(&rtc_save, 1, sizeof(rtc_save), f)) { @@ -347,6 +936,9 @@ 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; exit: fclose(f); return; @@ -372,7 +964,6 @@ uint8_t GB_run(GB_gameboy_t *gb) gb->cycles_since_run = 0; GB_cpu_run(gb); if (gb->vblank_just_occured) { - GB_update_joyp(gb); GB_rtc_run(gb); GB_debugger_handle_async_commands(gb); GB_rewind_push(gb); @@ -417,7 +1008,7 @@ void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback) void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) { -#ifndef DISABLE_DEBUGGER +#ifndef GB_DISABLE_DEBUGGER if (gb->input_callback == default_input_callback) { gb->async_input_callback = NULL; } @@ -427,26 +1018,46 @@ void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) { -#ifndef DISABLE_DEBUGGER +#ifndef GB_DISABLE_DEBUGGER gb->async_input_callback = callback; #endif } +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}}}; +const GB_palette_t GB_PALETTE_GBL = {{{0x0a, 0x1c, 0x15}, {0x35, 0x78, 0x62}, {0x56, 0xb4, 0x95}, {0x7f, 0xe2, 0xc3}, {0x91, 0xea, 0xd0}}}; + +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->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->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->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->rgb_encode_callback(gb, palette->colors[0].r, palette->colors[0].g, palette->colors[0].b); + + // LCD off color + gb->background_palettes_rgb[4] = + gb->rgb_encode_callback(gb, palette->colors[4].r, palette->colors[4].g, palette->colors[4].b); + } +} + +void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette) +{ + gb->dmg_palette = palette; + update_dmg_palette(gb); +} void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback) { - if (!gb->rgb_encode_callback && !GB_is_cgb(gb)) { - gb->sprite_palettes_rgb[4] = gb->sprite_palettes_rgb[0] = gb->background_palettes_rgb[0] = - callback(gb, 0xFF, 0xFF, 0xFF); - gb->sprite_palettes_rgb[5] = gb->sprite_palettes_rgb[1] = gb->background_palettes_rgb[1] = - callback(gb, 0xAA, 0xAA, 0xAA); - gb->sprite_palettes_rgb[6] = gb->sprite_palettes_rgb[2] = gb->background_palettes_rgb[2] = - callback(gb, 0x55, 0x55, 0x55); - gb->sprite_palettes_rgb[7] = gb->sprite_palettes_rgb[3] = gb->background_palettes_rgb[3] = - callback(gb, 0, 0, 0); - } gb->rgb_encode_callback = callback; + update_dmg_palette(gb); for (unsigned i = 0; i < 32; i++) { GB_palette_changed(gb, true, i * 2); @@ -466,7 +1077,7 @@ void GB_set_infrared_input(GB_gameboy_t *gb, bool state) gb->ir_queue_length = 0; } -void GB_queue_infrared_input(GB_gameboy_t *gb, bool state, long cycles_after_previous_change) +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"); @@ -526,7 +1137,7 @@ void GB_disconnect_serial(GB_gameboy_t *gb) bool GB_is_inited(GB_gameboy_t *gb) { - return gb->magic == 'SAME'; + return gb->magic == state_magic(); } bool GB_is_cgb(GB_gameboy_t *gb) @@ -535,6 +1146,11 @@ bool GB_is_cgb(GB_gameboy_t *gb) } bool GB_is_sgb(GB_gameboy_t *gb) +{ + return (gb->model & ~GB_MODEL_PAL_BIT & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB || (gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB2; +} + +bool GB_is_hle_sgb(GB_gameboy_t *gb) { return (gb->model & ~GB_MODEL_PAL_BIT) == GB_MODEL_SGB || gb->model == GB_MODEL_SGB2; } @@ -566,28 +1182,31 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_CGB_E: case GB_MODEL_AGB: /* Unverified */ for (unsigned i = 0; i < gb->ram_size; i++) { - gb->ram[i] = (random() & 0xFF); + gb->ram[i] = GB_random(); } break; case GB_MODEL_DMG_B: case GB_MODEL_SGB_NTSC: /* Unverified*/ case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ for (unsigned i = 0; i < gb->ram_size; i++) { - gb->ram[i] = (random() & 0xFF); + gb->ram[i] = GB_random(); if (i & 0x100) { - gb->ram[i] &= random(); + gb->ram[i] &= GB_random(); } else { - gb->ram[i] |= random(); + gb->ram[i] |= GB_random(); } } break; case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: for (unsigned i = 0; i < gb->ram_size; i++) { gb->ram[i] = 0x55; - gb->ram[i] ^= random() & random() & random(); + gb->ram[i] ^= GB_random() & GB_random() & GB_random(); } break; @@ -597,20 +1216,110 @@ static void reset_ram(GB_gameboy_t *gb) gb->ram[i] = 0; } else { - gb->ram[i] = (random() | random() | random() | random()) & 0xFF; + gb->ram[i] = GB_random() | GB_random() | GB_random() | GB_random(); } } break; } + /* HRAM */ + switch (gb->model) { + case GB_MODEL_CGB_C: + // case GB_MODEL_CGB_D: + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + for (unsigned i = 0; i < sizeof(gb->hram); i++) { + gb->hram[i] = GB_random(); + } + break; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: /* Unverified*/ + case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + for (unsigned i = 0; i < sizeof(gb->hram); i++) { + if (i & 1) { + gb->hram[i] = GB_random() | GB_random() | GB_random(); + } + else { + gb->hram[i] = GB_random() & GB_random() & GB_random(); + } + } + break; + } + + /* OAM */ + switch (gb->model) { + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + /* Zero'd out by boot ROM anyway*/ + break; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: /* Unverified */ + case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + for (unsigned i = 0; i < 8; i++) { + if (i & 2) { + gb->oam[i] = GB_random() & GB_random() & GB_random(); + } + else { + gb->oam[i] = GB_random() | GB_random() | GB_random(); + } + } + for (unsigned i = 8; i < sizeof(gb->oam); i++) { + gb->oam[i] = gb->oam[i - 8]; + } + break; + } + + /* Wave RAM */ + switch (gb->model) { + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + /* Initialized by CGB-A and newer, 0s in CGB-0*/ + break; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: /* Unverified*/ + case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + 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(); + } + else { + temp = 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; + } + } + for (unsigned i = 0; i < sizeof(gb->extra_oam); i++) { - gb->extra_oam[i] = (random() & 0xFF); + gb->extra_oam[i] = GB_random(); } if (GB_is_cgb(gb)) { for (unsigned i = 0; i < 64; i++) { - gb->background_palettes_data[i] = random() & 0xFF; /* Doesn't really matter as the boot ROM overrides it anyway*/ - gb->sprite_palettes_data[i] = random() & 0xFF; + 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(); } for (unsigned i = 0; i < 32; i++) { GB_palette_changed(gb, true, i * 2); @@ -619,6 +1328,36 @@ static void reset_ram(GB_gameboy_t *gb) } } +static void request_boot_rom(GB_gameboy_t *gb) +{ + if (gb->boot_rom_load_callback) { + GB_boot_rom_t type = 0; + switch (gb->model) { + case GB_MODEL_DMG_B: + type = GB_BOOT_ROM_DMG; + break; + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: + type = GB_BOOT_ROM_SGB; + break; + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + type = GB_BOOT_ROM_SGB2; + break; + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_E: + type = GB_BOOT_ROM_CGB; + break; + case GB_MODEL_AGB: + type = GB_BOOT_ROM_AGB; + break; + } + gb->boot_rom_load_callback(gb, type); + } +} + void GB_reset(GB_gameboy_t *gb) { uint32_t mbc_ram_size = gb->mbc_ram_size; @@ -630,29 +1369,22 @@ void GB_reset(GB_gameboy_t *gb) gb->mbc_rom_bank = 1; gb->last_rtc_second = time(NULL); gb->cgb_ram_bank = 1; - gb->io_registers[GB_IO_JOYP] = 0xF; + gb->io_registers[GB_IO_JOYP] = 0xCF; gb->mbc_ram_size = mbc_ram_size; if (GB_is_cgb(gb)) { - gb->ram_size = 0x2000 * 8; + gb->ram_size = 0x1000 * 8; gb->vram_size = 0x2000 * 2; memset(gb->vram, 0, gb->vram_size); gb->cgb_mode = true; + gb->object_priority = GB_OBJECT_PRIORITY_INDEX; } else { gb->ram_size = 0x2000; gb->vram_size = 0x2000; memset(gb->vram, 0, gb->vram_size); + gb->object_priority = GB_OBJECT_PRIORITY_X; - if (gb->rgb_encode_callback) { - gb->sprite_palettes_rgb[4] = gb->sprite_palettes_rgb[0] = gb->background_palettes_rgb[0] = - gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF); - gb->sprite_palettes_rgb[5] = gb->sprite_palettes_rgb[1] = gb->background_palettes_rgb[1] = - gb->rgb_encode_callback(gb, 0xAA, 0xAA, 0xAA); - gb->sprite_palettes_rgb[6] = gb->sprite_palettes_rgb[2] = gb->background_palettes_rgb[2] = - gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55); - gb->sprite_palettes_rgb[7] = gb->sprite_palettes_rgb[3] = gb->background_palettes_rgb[3] = - gb->rgb_encode_callback(gb, 0, 0, 0); - } + update_dmg_palette(gb); } reset_ram(gb); @@ -666,7 +1398,7 @@ void GB_reset(GB_gameboy_t *gb) gb->accessed_oam_row = -1; - if (GB_is_sgb(gb)) { + if (GB_is_hle_sgb(gb)) { if (!gb->sgb) { gb->sgb = malloc(sizeof(*gb->sgb)); } @@ -697,14 +1429,15 @@ void GB_reset(GB_gameboy_t *gb) gb->nontrivial_jump_state = NULL; } - gb->magic = (uintptr_t)'SAME'; + gb->magic = state_magic(); + request_boot_rom(gb); } void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model) { gb->model = model; if (GB_is_cgb(gb)) { - gb->ram = realloc(gb->ram, gb->ram_size = 0x2000 * 8); + gb->ram = realloc(gb->ram, gb->ram_size = 0x1000 * 8); gb->vram = realloc(gb->vram, gb->vram_size = 0x2000 * 2); } else { @@ -713,6 +1446,7 @@ void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model) } GB_rewind_free(gb); GB_reset(gb); + load_default_border(gb); } void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *size, uint16_t *bank) @@ -789,26 +1523,96 @@ void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier) uint32_t GB_get_clock_rate(GB_gameboy_t *gb) { - if (gb->model == GB_MODEL_SGB_NTSC) { - return SGB_NTSC_FREQUENCY * gb->clock_multiplier; - } - if (gb->model == GB_MODEL_SGB_PAL) { + if (gb->model & GB_MODEL_PAL_BIT) { return SGB_PAL_FREQUENCY * gb->clock_multiplier; } + if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) { + return SGB_NTSC_FREQUENCY * gb->clock_multiplier; + } return CPU_FREQUENCY * gb->clock_multiplier; } -size_t GB_get_screen_width(GB_gameboy_t *gb) +void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode) { - return GB_is_sgb(gb)? 256 : 160; + if (gb->border_mode > GB_BORDER_ALWAYS) return; + gb->border_mode = border_mode; } -size_t GB_get_screen_height(GB_gameboy_t *gb) +unsigned GB_get_screen_width(GB_gameboy_t *gb) { - return GB_is_sgb(gb)? 224 : 144; + switch (gb->border_mode) { + default: + case GB_BORDER_SGB: + return GB_is_hle_sgb(gb)? 256 : 160; + case GB_BORDER_NEVER: + return 160; + case GB_BORDER_ALWAYS: + return 256; + } +} + +unsigned GB_get_screen_height(GB_gameboy_t *gb) +{ + switch (gb->border_mode) { + default: + case GB_BORDER_SGB: + return GB_is_hle_sgb(gb)? 224 : 144; + case GB_BORDER_NEVER: + return 144; + case GB_BORDER_ALWAYS: + return 224; + } } unsigned GB_get_player_count(GB_gameboy_t *gb) { - return GB_is_sgb(gb)? gb->sgb->player_count : 1; + return GB_is_hle_sgb(gb)? gb->sgb->player_count : 1; +} + +void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback) +{ + gb->update_input_hint_callback = callback; +} + +double GB_get_usual_frame_rate(GB_gameboy_t *gb) +{ + return GB_get_clock_rate(gb) / (double)LCDC_PERIOD; +} + +void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback) +{ + gb->joyp_write_callback = callback; +} + +void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback) +{ + gb->icd_pixel_callback = callback; +} + +void GB_set_icd_hreset_callback(GB_gameboy_t *gb, GB_icd_hreset_callback_t callback) +{ + gb->icd_hreset_callback = callback; +} + + +void GB_set_icd_vreset_callback(GB_gameboy_t *gb, GB_icd_vreset_callback_t callback) +{ + gb->icd_vreset_callback = callback; +} + +void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t callback) +{ + gb->boot_rom_load_callback = callback; + request_boot_rom(gb); +} + +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 (current_time > alarm_time) return 0; + return alarm_time - current_time; } diff --git a/Core/gb.h b/Core/gb.h index f793f30..b773ebb 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -1,5 +1,6 @@ #ifndef GB_h #define GB_h +#define typeof __typeof__ #include #include #include @@ -20,15 +21,27 @@ #include "sm83_cpu.h" #include "symbol_hash.h" #include "sgb.h" +#include "cheats.h" +#include "rumble.h" #define GB_STRUCT_VERSION 13 -#ifdef GB_INTERNAL #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 #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ @@ -39,6 +52,17 @@ #error Unable to detect endianess #endif +typedef struct { + struct { + uint8_t r, g, b; + } colors[5]; +} GB_palette_t; + +extern const GB_palette_t GB_PALETTE_GREY; +extern const GB_palette_t GB_PALETTE_DMG; +extern const GB_palette_t GB_PALETTE_MGB; +extern const GB_palette_t GB_PALETTE_GBL; + typedef union { struct { uint8_t seconds; @@ -50,7 +74,6 @@ typedef union { uint8_t data[5]; } GB_rtc_time_t; - typedef enum { // GB_MODEL_DMG_0 = 0x000, // GB_MODEL_DMG_A = 0x001, @@ -58,9 +81,13 @@ typedef enum { // GB_MODEL_DMG_C = 0x003, GB_MODEL_SGB = 0x004, GB_MODEL_SGB_NTSC = GB_MODEL_SGB, - GB_MODEL_SGB_PAL = 0x1004, + GB_MODEL_SGB_PAL = GB_MODEL_SGB | GB_MODEL_PAL_BIT, + 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_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, @@ -83,10 +110,16 @@ enum { enum { GB_CARRY_FLAG = 16, GB_HALF_CARRY_FLAG = 32, - GB_SUBSTRACT_FLAG = 64, + GB_SUBTRACT_FLAG = 64, GB_ZERO_FLAG = 128, }; +typedef enum { + GB_BORDER_SGB, + GB_BORDER_NEVER, + GB_BORDER_ALWAYS, +} GB_border_mode_t; + #define GB_MAX_IR_QUEUE 256 enum { @@ -154,7 +187,7 @@ enum { // 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. - GB_IO_DMG_EMULATION = 0x4c, + GB_IO_KEY0 = 0x4c, /* General CGB features */ GB_IO_KEY1 = 0x4d, // CGB Mode Only - Prepare Speed Switch @@ -162,7 +195,7 @@ enum { /* Missing */ GB_IO_VBK = 0x4f, // CGB Mode Only - VRAM Bank - GB_IO_BIOS = 0x50, // Write to disable the BIOS mapping + GB_IO_BANK = 0x50, // Write to disable the BIOS mapping /* CGB DMA */ GB_IO_HDMA1 = 0x51, // CGB Mode Only - New DMA Source, High @@ -181,9 +214,7 @@ enum { 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 - - // 1 is written for DMG ROMs on a CGB. Does not appear to have an effect. - GB_IO_DMG_EMULATION_INDICATION = 0x6c, // (FEh) Bit 0 (Read/Write) + GB_IO_OPRI = 0x6c, // Affects object priority (X based or index based) /* Missing */ @@ -204,6 +235,17 @@ typedef enum { GB_LOG_UNDERLINE_MASK = GB_LOG_DASHED_UNDERLINE | GB_LOG_UNDERLINE } GB_log_attributes; +typedef enum { + GB_BOOT_ROM_DMG0, + GB_BOOT_ROM_DMG, + GB_BOOT_ROM_MGB, + GB_BOOT_ROM_SGB, + GB_BOOT_ROM_SGB2, + GB_BOOT_ROM_CGB0, + GB_BOOT_ROM_CGB, + GB_BOOT_ROM_AGB, +} GB_boot_rom_t; + #ifdef GB_INTERNAL #define LCDC_PERIOD 70224 #define CPU_FREQUENCY 0x400000 @@ -213,11 +255,11 @@ typedef enum { #define INTERNAL_DIV_CYCLES (0x40000) #if !defined(MIN) -#define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) #endif #if !defined(MAX) -#define MAX(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) +#define MAX(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) #endif #endif @@ -225,14 +267,20 @@ 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, long cycles_since_last_update); -typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, bool rumble_on); +typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on, uint64_t cycles_since_last_update); +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); +typedef void (*GB_update_input_hint_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_joyp_write_callback_t)(GB_gameboy_t *gb, uint8_t value); +typedef void (*GB_icd_pixel_callback_t)(GB_gameboy_t *gb, uint8_t row); +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; - long delay; + uint64_t delay; } GB_ir_queue_item_t; struct GB_breakpoint_s; @@ -258,10 +306,6 @@ typedef struct { This struct is not packed, but dumped sections exclusively use types that have the same alignment in both 32 and 64 bit platforms. */ -/* We make sure bool is 1 for cross-platform save state compatibility. */ -/* Todo: We might want to typedef our own bool if this prevents SameBoy from working on specific platforms. */ -_Static_assert(sizeof(bool) == 1, "sizeof(bool) != 1"); - #ifdef GB_INTERNAL struct GB_gameboy_s { #else @@ -323,6 +367,7 @@ struct GB_gameboy_internal_s { bool infrared_input; GB_printer_t printer; uint8_t extra_oam[0xff00 - 0xfea0]; + uint32_t ram_size; // Different between CGB and DMG ); /* DMA and HDMA */ @@ -360,9 +405,8 @@ struct GB_gameboy_internal_s { } mbc2; struct { - uint8_t rom_bank:7; - uint8_t padding:1; - uint8_t ram_bank:4; + uint8_t rom_bank:8; + uint8_t ram_bank:3; } mbc3; struct { @@ -374,18 +418,30 @@ struct GB_gameboy_internal_s { struct { uint8_t bank_low:6; uint8_t bank_high:3; - uint8_t mode:1; + bool mode:1; + bool ir_mode:1; } huc1; struct { - uint8_t rom_bank; - uint8_t ram_bank; + uint8_t rom_bank:7; + uint8_t padding:1; + uint8_t ram_bank:4; } huc3; }; uint16_t mbc_rom0_bank; /* For some MBC1 wirings. */ bool camera_registers_mapped; uint8_t camera_registers[0x36]; bool rumble_state; + bool cart_ir; + + // TODO: move to huc3 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; ); @@ -429,11 +485,12 @@ struct GB_gameboy_internal_s { uint8_t position_in_line; bool stat_interrupt_line; uint8_t effective_scx; - uint8_t wy_diff; + uint8_t window_y; /* The LCDC will skip the first frame it renders after turning it on. On the CGB, a frame is not skipped if the previous frame was skipped as well. See https://www.reddit.com/r/EmuDev/comments/6exyxu/ */ - /* TODO: Drop this and properly emulate the dropped vreset signal*/ + + /* TODO: Drop this and properly emulate the dropped vreset signal*/ enum { GB_FRAMESKIP_LCD_TURNED_ON, // On a DMG, the LCD renders a blank screen during this state, // on a CGB, the previous frame is repeated (which might be @@ -445,7 +502,7 @@ struct GB_gameboy_internal_s { bool vram_read_blocked; bool oam_write_blocked; bool vram_write_blocked; - bool window_disabled_while_active; + bool fifo_insertion_glitch; uint8_t current_line; uint16_t ly_for_comparison; GB_fifo_t bg_fifo, oam_fifo; @@ -456,9 +513,9 @@ struct GB_gameboy_internal_s { uint8_t current_tile_attributes; uint8_t current_tile_data[2]; uint8_t fetcher_state; - bool bg_fifo_paused; - bool oam_fifo_paused; - bool in_window; + bool window_is_being_fetched; + bool wx166_glitch; + bool wx_triggered; uint8_t visible_objs[10]; uint8_t obj_comparators[10]; uint8_t n_visible_objs; @@ -469,6 +526,21 @@ struct GB_gameboy_internal_s { 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; + bool cgb_palettes_ppu_blocked; + bool object_fetch_aborted; + bool during_object_fetch; + uint16_t object_low_line_address; + bool wy_triggered; + uint8_t window_tile_x; + uint8_t lcd_x; // The LCD can go out of sync since the push signal is skipped in some cases. + bool is_odd_frame; + uint16_t last_tile_data_address; + uint16_t last_tile_index_address; + bool cgb_repeated_a_frame; ); /* Unsaved data. This includes all pointers, as well as everything that shouldn't be on a save state */ @@ -482,6 +554,7 @@ struct GB_gameboy_internal_s { GB_STANDARD_MBC1_WIRING, GB_MBC1M_WIRING, } mbc1_wiring; + bool is_mbc30; unsigned pending_cycles; @@ -494,8 +567,13 @@ struct GB_gameboy_internal_s { uint32_t *screen; uint32_t background_palettes_rgb[0x20]; uint32_t sprite_palettes_rgb[0x20]; + const GB_palette_t *dmg_palette; GB_color_correction_mode_t color_correction_mode; bool keys[4][GB_KEY_MAX]; + GB_border_mode_t border_mode; + GB_sgb_border_t borrowed_border; + bool tried_loading_sgb_border; + bool has_sgb_border; /* Timing */ uint64_t last_sync; @@ -517,10 +595,17 @@ struct GB_gameboy_internal_s { GB_rumble_callback_t rumble_callback; GB_serial_transfer_bit_start_callback_t serial_transfer_bit_start_callback; GB_serial_transfer_bit_end_callback_t serial_transfer_bit_end_callback; - + GB_update_input_hint_callback_t update_input_hint_callback; + GB_joyp_write_callback_t joyp_write_callback; + GB_icd_pixel_callback_t icd_pixel_callback; + 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_boot_rom_load_callback_t boot_rom_load_callback; + GB_print_image_callback_t printer_callback; /* IR */ - long cycles_since_ir_change; // In 8MHz units - long cycles_since_input_ir_change; // In 8MHz units + 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; @@ -531,18 +616,18 @@ struct GB_gameboy_internal_s { /* Breakpoints */ uint16_t n_breakpoints; struct GB_breakpoint_s *breakpoints; - bool has_jump_to_breakpoints; + bool has_jump_to_breakpoints, has_software_breakpoints; void *nontrivial_jump_state; bool non_trivial_jump_breakpoint_occured; /* SLD (Todo: merge with backtrace) */ bool stack_leak_detection; - int debug_call_depth; + signed debug_call_depth; uint16_t sp_for_call_depth[0x200]; /* Should be much more than enough */ uint16_t addr_for_call_depth[0x200]; /* Backtrace */ - unsigned int backtrace_size; + unsigned backtrace_size; uint16_t backtrace_sps[0x200]; struct { uint16_t bank; @@ -558,7 +643,7 @@ struct GB_gameboy_internal_s { GB_reversed_symbol_map_t reversed_symbol_map; /* Ticks command */ - unsigned long debugger_ticks; + uint64_t debugger_ticks; /* Rewind */ #define GB_REWIND_FRAMES_PER_KEY 255 @@ -576,16 +661,27 @@ struct GB_gameboy_internal_s { double sgb_intro_jingle_phases[7]; double sgb_intro_sweep_phase; double sgb_intro_sweep_previous_sample; + + /* Cheats */ + bool cheat_enabled; + size_t cheat_count; + GB_cheat_t **cheats; + GB_cheat_hash_t *cheat_hash[256]; /* Misc */ bool turbo; bool turbo_dont_skip; bool disable_rendering; - uint32_t ram_size; // Different between CGB and DMG uint8_t boot_rom[0x900]; bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank uint8_t cycles_since_run; // How many cycles have passed since the last call to GB_run(), in 8MHz units double clock_multiplier; + GB_rumble_mode_t rumble_mode; + uint32_t rumble_on_cycles; + uint32_t rumble_off_cycles; + + /* Temporary state */ + bool wx_just_changed; ); }; @@ -605,13 +701,14 @@ __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_sgb(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); void GB_free(GB_gameboy_t *gb); void GB_reset(GB_gameboy_t *gb); void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model); -/* Returns the time passed, in 4MHz ticks. */ +/* Returns the time passed, in 8MHz ticks. */ uint8_t GB_run(GB_gameboy_t *gb); /* Returns the time passed since the last frame, in nanoseconds */ uint64_t GB_run_frame(GB_gameboy_t *gb); @@ -642,8 +739,14 @@ 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_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); + +void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size); void GB_load_battery(GB_gameboy_t *gb, const char *path); void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip); @@ -653,9 +756,10 @@ void GB_log(GB_gameboy_t *gb, const char *fmt, ...) __printflike(2, 3); void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...) __printflike(3, 4); 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, long cycles_after_previous_change); /* In 8MHz units*/ +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); @@ -664,6 +768,11 @@ void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback); void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback); void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback); +void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback); +/* 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_palette(GB_gameboy_t *gb, const GB_palette_t *palette); /* 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); @@ -674,15 +783,24 @@ bool GB_serial_get_data_bit(GB_gameboy_t *gb); void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data); 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 integration with SFC/SNES emulators */ +void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback); +void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback); +void GB_set_icd_hreset_callback(GB_gameboy_t *gb, GB_icd_hreset_callback_t callback); +void GB_set_icd_vreset_callback(GB_gameboy_t *gb, GB_icd_vreset_callback_t callback); + #ifdef GB_INTERNAL uint32_t GB_get_clock_rate(GB_gameboy_t *gb); #endif void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier); -size_t GB_get_screen_width(GB_gameboy_t *gb); -size_t GB_get_screen_height(GB_gameboy_t *gb); - +unsigned GB_get_screen_width(GB_gameboy_t *gb); +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); #endif /* GB_h */ diff --git a/Core/graphics/agb_border.inc b/Core/graphics/agb_border.inc new file mode 100644 index 0000000..dd4ebbe --- /dev/null +++ b/Core/graphics/agb_border.inc @@ -0,0 +1,522 @@ +static const uint16_t palette[] = { + 0x410A, 0x0421, 0x35AD, 0x4A52, 0x7FFF, 0x2D49, 0x0C42, 0x1484, + 0x18A5, 0x20C6, 0x6718, 0x5D6E, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +static const uint16_t tilemap[] = { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0001, 0x0002, 0x0003, 0x0003, 0x0003, 0x0004, 0x0004, + 0x0004, 0x0004, 0x0004, 0x0004, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0004, 0x0004, 0x0004, 0x0004, + 0x0004, 0x0004, 0x0003, 0x0003, 0x0003, 0x4002, 0x4001, 0x0000, + 0x0000, 0x0006, 0x0007, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x4007, 0x4006, 0x0000, + 0x0000, 0x0009, 0x0008, 0x0008, 0x0008, 0x000A, 0x000B, 0x000B, + 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, + 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, + 0x000B, 0x000B, 0x400A, 0x0008, 0x0008, 0x0008, 0xC009, 0x0000, + 0x0000, 0x000C, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x400C, 0x0000, + 0x0000, 0x000E, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC00E, 0x0000, + 0x0000, 0x000F, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x400F, 0x0000, + 0x0000, 0x0010, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC010, 0x0000, + 0x0000, 0x0010, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC010, 0x0000, + 0x0000, 0x0011, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC011, 0x0000, + 0x0000, 0x0011, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC011, 0x0000, + 0x0000, 0x0012, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x4012, 0x0000, + 0x0000, 0x0013, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC013, 0x0000, + 0x0014, 0x0015, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x4015, 0x4014, + 0x0016, 0x0017, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC017, 0xC016, + 0x0016, 0x0017, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC017, 0xC016, + 0x0018, 0x0019, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x4019, 0x4018, + 0x001A, 0x001B, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC01B, 0xC01A, + 0x001C, 0x001D, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x401D, 0x401C, + 0x001E, 0x0008, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x0008, 0xC01E, + 0x001E, 0x0008, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x0008, 0xC01E, + 0x001E, 0x0008, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x0008, 0xC01E, + 0x001F, 0x801D, 0x0008, 0x0008, 0x0008, 0x0020, 0x0021, 0x0022, + 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, + 0x002B, 0x002C, 0x002D, 0x002D, 0x002D, 0x002D, 0x002D, 0x002D, + 0x002E, 0x0021, 0x4020, 0x0008, 0x0008, 0x0008, 0xC01D, 0x401F, + 0x002F, 0x0030, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0031, + 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, + 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, 0x0040, 0x0041, + 0x0042, 0x0043, 0x0008, 0x0008, 0x0008, 0x0008, 0x4030, 0x402F, + 0x0044, 0x0045, 0x0046, 0x0047, 0x0008, 0x0008, 0x0048, 0x0049, + 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050, 0x0051, + 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, + 0x005A, 0x005B, 0x0008, 0x0008, 0x4047, 0x4046, 0x4045, 0x4044, + 0x0000, 0x0000, 0x005C, 0x005D, 0x005E, 0x005F, 0x0060, 0x0061, + 0x0061, 0x0062, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, + 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x4062, 0x0061, + 0x0061, 0x4060, 0x405F, 0x405E, 0x405D, 0x405C, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +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, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1F, 0x01, + 0x7F, 0x1F, 0xFF, 0x7E, 0xFF, 0xE1, 0xFF, 0x9F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x1E, + 0x1F, 0x60, 0x7F, 0x80, 0xFF, 0x00, 0xBF, 0x40, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x03, 0x01, 0x07, 0x03, 0x07, 0x03, 0x07, 0x06, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x01, 0x02, 0x03, 0x04, 0x03, 0x04, 0x06, 0x01, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0xFF, 0xBF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBF, 0x40, 0x7F, 0x80, 0x7F, 0x80, 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, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, + 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, + 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, + 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFE, 0x00, + 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, 0xFF, 0x00, 0x00, 0x00, + 0x0F, 0x0E, 0x0F, 0x0E, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x0E, 0x01, 0x0E, 0x01, 0x1B, 0x04, 0x1B, 0x04, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, + 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, + 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, + 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1F, 0x1B, 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0x1B, 0x04, 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, + 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, + 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, + 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, + 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, + 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, + 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, + 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, + 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, + 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0xFF, 0xDF, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, 0xDF, 0x20, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xBF, 0xFF, 0xBF, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0xDF, 0x20, 0xDF, 0x20, 0xBF, 0x40, 0xBF, 0x40, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, + 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, + 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, + 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, + 0x01, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x01, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0xFF, 0xBF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xBF, 0x40, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, + 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0x01, 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, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF8, 0xFF, 0xE7, 0xF8, 0xDF, 0xE3, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xF8, 0x00, 0xE4, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0xFF, 0xCE, 0x3F, 0xF5, 0x8E, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0x00, 0x4E, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x1F, 0xFF, 0xEE, 0x1F, 0xB5, 0x4E, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x1F, 0x00, 0x0E, 0xA0, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x07, 0xFF, 0xFB, 0x07, 0x04, 0x73, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x07, 0x00, 0x03, 0x88, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x80, 0xFF, 0x7F, 0x80, 0x82, 0x39, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x80, 0x00, 0x01, 0x44, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x01, 0xFE, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x83, 0x7C, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x42, 0x01, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xFF, 0xBB, 0x7C, 0x4F, 0xB0, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x7C, 0x00, 0xB1, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x07, 0xFF, 0xF9, 0x06, 0xE7, 0xF8, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x06, 0x00, 0x08, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x0E, 0xFF, 0xF5, 0x0E, 0x9B, 0x74, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x0E, 0x00, 0x94, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x1F, 0xFF, 0xF7, 0x0F, 0xBF, 0x47, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x0F, 0x00, 0xA7, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 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, 0x01, 0xFF, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x03, 0x03, 0x03, 0x03, 0x01, 0x01, + 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, + 0x03, 0x04, 0x01, 0x02, 0x01, 0x02, 0x00, 0x01, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xDF, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0xBF, 0x40, 0xBF, 0x40, 0xDF, 0x20, + 0xB0, 0xD8, 0xA0, 0xD3, 0x67, 0x84, 0x47, 0xA4, + 0x61, 0x81, 0xA0, 0xD0, 0xB4, 0xCA, 0x7E, 0x81, + 0xD7, 0x08, 0xCC, 0x13, 0x98, 0x20, 0x98, 0x00, + 0x9E, 0x20, 0xCF, 0x00, 0xCD, 0x02, 0x80, 0x01, + 0x32, 0x2D, 0x13, 0x6D, 0x34, 0x48, 0xFC, 0x02, + 0x7C, 0x00, 0x78, 0x05, 0x30, 0x49, 0x20, 0x50, + 0xCD, 0x00, 0xAC, 0x40, 0x49, 0x82, 0x01, 0x02, + 0x07, 0x80, 0xC2, 0x05, 0x86, 0x41, 0x9F, 0x40, + 0x15, 0x2E, 0x09, 0x06, 0x09, 0x16, 0x0B, 0xD4, + 0xC6, 0x49, 0x8E, 0x40, 0xCF, 0xC8, 0x06, 0x01, + 0xCE, 0x20, 0xE6, 0x10, 0xE6, 0x00, 0x24, 0xD0, + 0x39, 0x80, 0x38, 0x01, 0x31, 0x00, 0xF8, 0x00, + 0x0C, 0x8B, 0x85, 0x8A, 0x03, 0x84, 0x27, 0x20, + 0x22, 0x35, 0x12, 0x34, 0x20, 0x12, 0x10, 0x20, + 0x73, 0x00, 0x72, 0x08, 0x7C, 0x80, 0xDC, 0x01, + 0xC8, 0x11, 0xC9, 0x06, 0xCD, 0x22, 0xEF, 0x10, + 0x83, 0x44, 0x86, 0x01, 0x03, 0x85, 0x26, 0x21, + 0x46, 0x69, 0x46, 0x68, 0x8E, 0xCA, 0x86, 0x88, + 0x39, 0x40, 0x78, 0x84, 0x7C, 0x80, 0xD8, 0x01, + 0x90, 0x29, 0xD1, 0x28, 0x73, 0x00, 0xB3, 0x40, + 0x00, 0x01, 0x01, 0x00, 0x3F, 0x00, 0x3F, 0x40, + 0x03, 0x02, 0x01, 0x02, 0x41, 0x7C, 0x7F, 0x00, + 0xFE, 0x00, 0xFF, 0x00, 0xC0, 0x00, 0x80, 0x40, + 0xFC, 0x00, 0xFC, 0x00, 0x80, 0x02, 0xC0, 0x00, + 0xC0, 0x00, 0x80, 0x4C, 0xCC, 0x43, 0x8E, 0x52, + 0x80, 0x4C, 0x80, 0x00, 0x12, 0x1E, 0x9E, 0x00, + 0x7F, 0x00, 0x33, 0x0C, 0x32, 0x01, 0x23, 0x50, + 0x33, 0x4C, 0x7F, 0x00, 0x61, 0x80, 0xF1, 0x00, + 0x7C, 0x02, 0x30, 0x48, 0x31, 0x40, 0x61, 0x50, + 0x87, 0xE4, 0xE3, 0x84, 0x23, 0x44, 0x43, 0x44, + 0x85, 0x42, 0x87, 0x40, 0x8F, 0x50, 0x8C, 0x12, + 0x78, 0x00, 0x18, 0x20, 0xB8, 0x00, 0x98, 0x24, + 0x03, 0x04, 0x03, 0xE0, 0xF1, 0x12, 0xF0, 0x09, + 0xF9, 0x09, 0xF9, 0x08, 0xE1, 0x12, 0xF1, 0x12, + 0xF8, 0x00, 0x1E, 0xE0, 0x0C, 0x02, 0x07, 0x08, + 0x07, 0x00, 0x06, 0x00, 0x1C, 0x02, 0x0C, 0x00, + 0x9F, 0x91, 0x86, 0x88, 0xC4, 0x4C, 0x80, 0x4C, + 0xE1, 0x20, 0xC1, 0x22, 0x23, 0xD4, 0x22, 0xD5, + 0x60, 0x00, 0xFB, 0x00, 0x37, 0x00, 0x73, 0x0C, + 0x1F, 0x00, 0x3C, 0x00, 0xC8, 0x14, 0xC9, 0x14, + 0x16, 0x2F, 0x76, 0x4F, 0x2D, 0xDE, 0xDD, 0xBE, + 0xBA, 0x7D, 0x7A, 0xFD, 0x7A, 0xFD, 0xF4, 0xF8, + 0xCF, 0x00, 0x8F, 0x00, 0x5E, 0x80, 0xBE, 0x00, + 0x7D, 0x00, 0xFC, 0x00, 0xFC, 0x01, 0xF9, 0x02, + 0xFF, 0x00, 0xBF, 0x78, 0x86, 0x09, 0x86, 0x89, + 0x06, 0x25, 0x02, 0x25, 0x42, 0x45, 0x60, 0x11, + 0x00, 0x00, 0x09, 0x00, 0x70, 0x81, 0x70, 0x09, + 0xDC, 0x21, 0xD8, 0x01, 0x98, 0x25, 0xCC, 0x13, + 0xFF, 0x00, 0xF3, 0xF8, 0x02, 0x03, 0x01, 0x30, + 0x39, 0x09, 0x30, 0x09, 0x31, 0x09, 0x20, 0x19, + 0x00, 0x00, 0x01, 0x04, 0xFC, 0x00, 0xCF, 0x30, + 0xE6, 0x00, 0xE6, 0x01, 0xE6, 0x00, 0xF6, 0x08, + 0xFF, 0x00, 0xFA, 0xC7, 0x18, 0x21, 0x09, 0x10, + 0x88, 0x99, 0x93, 0x1A, 0x83, 0x11, 0xC2, 0x41, + 0x00, 0x00, 0x00, 0x20, 0xC6, 0x21, 0xFF, 0x00, + 0x67, 0x00, 0xE4, 0x08, 0x6F, 0x10, 0x3C, 0x00, + 0xFD, 0x02, 0xB5, 0x3A, 0xC7, 0x44, 0x03, 0x84, + 0x83, 0x24, 0x21, 0xB0, 0x21, 0x12, 0x21, 0x02, + 0x02, 0x00, 0x02, 0x40, 0x3C, 0x00, 0xF8, 0x00, + 0xD8, 0x24, 0x4C, 0x92, 0xEC, 0x00, 0xCC, 0x12, + 0xFF, 0x00, 0xFF, 0xF3, 0x1C, 0x14, 0x0C, 0x04, + 0x00, 0x0C, 0x04, 0x24, 0x00, 0x24, 0x10, 0x30, + 0x00, 0x00, 0x10, 0x04, 0xE3, 0x00, 0xFB, 0x00, + 0xF3, 0x08, 0xDB, 0x20, 0xDB, 0x04, 0xCF, 0x00, + 0xFF, 0x00, 0xEC, 0x3E, 0xC1, 0x01, 0x01, 0x8E, + 0x8F, 0x10, 0x0F, 0x90, 0x0F, 0x90, 0x0D, 0x09, + 0x00, 0x00, 0x20, 0x01, 0x7E, 0x00, 0xF1, 0x0E, + 0xE0, 0x10, 0x60, 0x10, 0x60, 0x10, 0x79, 0x82, + 0xFF, 0x00, 0x7F, 0xFC, 0x03, 0x82, 0x01, 0x9E, + 0x13, 0x80, 0x03, 0x80, 0x03, 0x9C, 0x0F, 0x90, + 0x00, 0x00, 0x02, 0x00, 0x7C, 0x80, 0x60, 0x9C, + 0x60, 0x9C, 0x7C, 0x80, 0x60, 0x9C, 0x70, 0x80, + 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, + 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xEF, 0xFF, 0xF7, 0x7F, 0x7B, 0x3F, 0x3C, + 0x1F, 0x1F, 0x0F, 0x0F, 0x03, 0x03, 0x00, 0x00, + 0xEF, 0x10, 0x77, 0x88, 0x3B, 0x44, 0x1C, 0x23, + 0x0F, 0x10, 0x03, 0x0C, 0x00, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x3F, 0xFF, 0xC3, 0xFF, 0xFC, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x3F, 0xC0, 0xC3, 0x3C, 0xFC, 0x03, 0x3F, 0xC0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xC1, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0xC0, 0xC1, 0x3E, + 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 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, 0xFF, 0x00, + 0xEA, 0x14, 0xC0, 0x00, 0x80, 0x21, 0x7F, 0x92, + 0x9F, 0xE0, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x27, 0x18, 0x7F, 0x00, 0x1E, 0x61, 0x9A, 0x04, + 0xE0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x73, 0x53, 0x47, 0x44, 0x46, 0x25, 0xFD, 0x03, + 0xF9, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x8C, 0x00, 0xD8, 0x20, 0x1D, 0xA0, 0x03, 0x00, + 0x07, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC7, 0xE1, 0xE6, 0x05, 0x42, 0xA5, 0xBF, 0xC0, + 0x9F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x18, 0x24, 0x38, 0x01, 0xB8, 0x05, 0xC0, 0x00, + 0xE0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x21, 0x11, 0x31, 0x49, 0x33, 0x4A, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xDE, 0x00, 0x87, 0x48, 0x84, 0x48, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xCC, 0x02, 0x8E, 0x4A, 0xCC, 0x42, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x71, 0x08, 0x39, 0x00, 0x31, 0x02, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x3D, 0x40, 0x03, 0x02, 0x03, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBC, 0x02, 0xFC, 0x00, 0xFE, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x12, 0x82, 0x80, 0x80, 0x01, 0x83, 0xFF, 0x00, + 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x61, 0x1C, 0x7F, 0x00, 0x7C, 0x82, 0x00, 0x00, + 0x01, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x22, 0x52, 0x30, 0xC0, 0x58, 0xA4, 0x8F, 0x72, + 0x73, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x8C, 0x11, 0x4F, 0x90, 0xA3, 0x0C, 0x73, 0x00, + 0xFC, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x23, 0xA4, 0x06, 0x0D, 0x05, 0x1B, 0xBB, 0x07, + 0xE7, 0x1F, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x98, 0x44, 0xF5, 0x08, 0xEB, 0x00, 0x87, 0x40, + 0x1F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x66, 0x85, 0xE2, 0xA5, 0x66, 0x81, 0xBF, 0xC1, + 0x99, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x99, 0x00, 0xB9, 0x00, 0x9D, 0x20, 0xC1, 0x00, + 0xE7, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xF6, 0xFA, 0xFC, 0xF2, 0xF7, 0xF8, 0xFB, 0xFC, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF9, 0x00, 0xF1, 0x02, 0xF8, 0x00, 0xFC, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x52, 0x53, 0x30, 0x23, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x8C, 0x21, 0xCC, 0x13, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x03, 0x03, 0x06, 0xFE, 0x01, 0xF9, 0x07, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0x02, 0xFA, 0x04, 0x01, 0x00, 0x07, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x86, 0x05, 0x46, 0xA0, 0x5F, 0xB8, 0xBF, 0xC0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x38, 0x41, 0x99, 0x26, 0xB8, 0x00, 0xC0, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x30, 0x28, 0x09, 0x09, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC6, 0x09, 0xE6, 0x10, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x20, 0x38, 0x38, 0x20, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xD7, 0x08, 0xCF, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x80, 0x41, 0xA1, 0x61, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3E, 0x40, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x01, 0x82, 0x01, 0x82, 0xFF, 0x00, 0xFC, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7C, 0x82, 0x7C, 0x82, 0x00, 0x00, 0x03, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 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, 0xFF, 0x00, + 0x3F, 0x3F, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x3C, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFE, 0xFF, 0xFF, 0x3F, 0x3F, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFE, 0x01, 0x3F, 0xC0, 0x01, 0x3E, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x3F, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0xC0, 0xC0, 0x3F, 0xFF, 0x00, 0x3F, 0xC0, + 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xC0, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x3F, 0xC0, 0xC0, 0x3F, 0xFF, 0x00, + 0x3F, 0xC0, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFC, + 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x03, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0xFC, 0x03, + 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, + 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x03, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x03, 0xFC, + 0xFC, 0x03, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, +}; diff --git a/Core/graphics/cgb_border.inc b/Core/graphics/cgb_border.inc new file mode 100644 index 0000000..755312a --- /dev/null +++ b/Core/graphics/cgb_border.inc @@ -0,0 +1,446 @@ +static const uint16_t palette[] = { + 0x7C1A, 0x0000, 0x0011, 0x3DEF, 0x6318, 0x7FFF, 0x1EBA, 0x19AF, + 0x1EAF, 0x4648, 0x7FC0, 0x2507, 0x1484, 0x5129, 0x5010, 0x2095, +}; + +static const uint16_t tilemap[] = { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0001, 0x0002, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x4002, 0x4001, 0x0000, + 0x0000, 0x0004, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x4004, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0007, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x4007, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x000A, 0x000B, 0x400A, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x800A, 0x000C, 0xC00A, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x000D, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x000E, + 0x000F, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, + 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, + 0x001F, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x400D, 0x0000, + 0x0000, 0x0020, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0021, + 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, + 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, 0x0030, 0x0031, + 0x0032, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x4020, 0x0000, + 0x0000, 0x0033, 0x0034, 0x0035, 0x0036, 0x0005, 0x0005, 0x0037, + 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, + 0x0040, 0x0005, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, + 0x0047, 0x0005, 0x0005, 0x4036, 0x4035, 0x4034, 0x4033, 0x0000, + 0x0000, 0x0000, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, + 0x004E, 0x004E, 0x004F, 0x0050, 0x0050, 0x0050, 0x0050, 0x0050, + 0x0050, 0x0050, 0x0050, 0x0050, 0x0050, 0x404F, 0x004E, 0x004E, + 0x404D, 0x004C, 0x404B, 0x404A, 0x4049, 0x4048, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 +}; + +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, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x08, + 0x01, 0x11, 0x06, 0x26, 0x04, 0x24, 0x08, 0x48, + 0x00, 0x00, 0x01, 0x01, 0x07, 0x07, 0x0F, 0x0F, + 0x1E, 0x1F, 0x39, 0x3F, 0x3B, 0x3F, 0x77, 0x7F, + 0x00, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x7F, 0x7F, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x08, 0x48, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x77, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x42, + 0xBD, 0xBD, 0x7E, 0x66, 0x7E, 0xFF, 0x7E, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBD, 0xFF, + 0x7E, 0xFF, 0xFF, 0xE7, 0x7E, 0x7E, 0x7E, 0x7E, + 0x7E, 0xFF, 0x3C, 0xFF, 0x81, 0xFF, 0xC3, 0xFF, + 0x7E, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7E, 0x7E, 0x3C, 0x3C, 0x00, 0x00, 0x00, 0x81, + 0x81, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xDF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x78, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xC7, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xE3, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x9F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x60, 0xE1, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8, 0xD8, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x8F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0x8F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0x9F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x0F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC3, 0xC2, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBD, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x10, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0x84, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x08, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, + 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, + 0x08, 0x48, 0x04, 0x24, 0x04, 0x24, 0x02, 0x12, + 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, + 0x37, 0x7F, 0x1B, 0x3F, 0x1B, 0x3F, 0x0D, 0x1F, + 0x0F, 0x08, 0x0E, 0x00, 0x1E, 0x12, 0x1E, 0x12, + 0x1F, 0x10, 0x0F, 0x08, 0x02, 0x02, 0x00, 0x00, + 0xF7, 0xF8, 0xFF, 0xF0, 0xED, 0xE3, 0xED, 0xE1, + 0xEF, 0xE0, 0xF7, 0xF0, 0xFD, 0xFC, 0xFF, 0xFF, + 0xF0, 0x10, 0x40, 0x00, 0x41, 0x41, 0x00, 0x00, + 0x83, 0x82, 0xE3, 0x20, 0xC7, 0x04, 0xC7, 0x00, + 0xEF, 0x1F, 0xFF, 0x1F, 0xBE, 0xFF, 0xFF, 0xFE, + 0x7D, 0x7E, 0xDF, 0x3C, 0xFB, 0x18, 0xFF, 0x18, + 0x60, 0x00, 0x70, 0x00, 0xF8, 0x08, 0xB0, 0x00, + 0xD8, 0x40, 0x3C, 0x24, 0x5C, 0x44, 0xFC, 0x00, + 0xFF, 0x8F, 0xFF, 0x0F, 0xF7, 0x07, 0xFF, 0x07, + 0xBF, 0x47, 0xDB, 0x47, 0xBB, 0x03, 0xFF, 0x03, + 0x3C, 0x04, 0x78, 0x00, 0x78, 0x00, 0xEC, 0x80, + 0xFE, 0x92, 0xE7, 0x83, 0xE5, 0x80, 0x4F, 0x08, + 0xFB, 0x83, 0xFF, 0x83, 0xFF, 0x83, 0x7F, 0x83, + 0x6D, 0x93, 0x7C, 0x10, 0x7F, 0x10, 0xF7, 0x10, + 0x3C, 0x00, 0x7C, 0x40, 0x78, 0x00, 0xC8, 0x80, + 0xFC, 0x24, 0xBC, 0x24, 0xFD, 0x65, 0x3D, 0x25, + 0xFF, 0xC3, 0xBF, 0x83, 0xFF, 0x83, 0x7F, 0x03, + 0xDB, 0x23, 0xDB, 0x23, 0x9A, 0x67, 0xDA, 0x47, + 0xFF, 0x80, 0xFF, 0x80, 0xE0, 0x80, 0x40, 0x00, + 0xFF, 0x01, 0xFF, 0x01, 0xDF, 0x1F, 0xE0, 0x20, + 0x7F, 0x80, 0x7F, 0x00, 0x7F, 0x1F, 0xFF, 0x1F, + 0xFE, 0x00, 0xFE, 0x00, 0xE0, 0x01, 0xDF, 0x3F, + 0xBF, 0xA0, 0xB9, 0xA0, 0x10, 0x00, 0x11, 0x01, + 0x3B, 0x00, 0x3F, 0x00, 0x7E, 0x4E, 0x78, 0x48, + 0x5F, 0x40, 0x5F, 0xC0, 0xFF, 0xC7, 0xFE, 0xC7, + 0xFF, 0xC0, 0xFF, 0xC0, 0xB1, 0xC4, 0xB7, 0x8F, + 0xE3, 0x22, 0xC7, 0x04, 0xCE, 0x08, 0xE6, 0x20, + 0xCE, 0x42, 0xDE, 0x52, 0xFE, 0x32, 0xFC, 0x30, + 0xDD, 0x3E, 0xFB, 0x18, 0xF7, 0x18, 0xDF, 0x31, + 0xBD, 0x31, 0xAD, 0x23, 0xCD, 0x31, 0xCF, 0x11, + 0xFE, 0x02, 0x9E, 0x00, 0x86, 0x80, 0x06, 0x00, + 0x03, 0x00, 0x02, 0x00, 0x07, 0x01, 0x07, 0x01, + 0xFD, 0x03, 0xFF, 0x01, 0x7F, 0xF0, 0xFF, 0xF8, + 0xFF, 0xF8, 0xFF, 0xF8, 0xFE, 0xF8, 0xFE, 0xF1, + 0x38, 0x08, 0x71, 0x41, 0x1F, 0x06, 0x39, 0x20, + 0x0F, 0x00, 0x0F, 0x01, 0x04, 0x00, 0x0C, 0x00, + 0xF7, 0x87, 0xBE, 0xC6, 0xF9, 0xC6, 0xDF, 0xE0, + 0xFF, 0xE0, 0xFE, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, + 0x70, 0x10, 0xE0, 0x20, 0x80, 0x00, 0x80, 0x00, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xEF, 0x1F, 0xDF, 0x1F, 0xFF, 0x3F, 0xFF, 0x7F, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3F, 0x3F, 0x7F, 0x7F, 0x78, 0x78, 0xF0, 0xF0, + 0xF0, 0xF0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF0, 0xF0, + 0xDF, 0xFF, 0xBD, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, + 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, + 0xE7, 0xE0, 0xFF, 0xF0, 0xFE, 0xF0, 0x1C, 0x00, + 0x3C, 0x20, 0x3C, 0x24, 0x3C, 0x24, 0x3C, 0x20, + 0xFF, 0xFF, 0xEF, 0xFF, 0x6F, 0xFF, 0xFF, 0xFF, + 0xDF, 0xFF, 0xDB, 0xFF, 0xDB, 0xFF, 0xDF, 0xFF, + 0xF8, 0x00, 0xFC, 0xC0, 0x1F, 0x03, 0x1F, 0x13, + 0x1F, 0x13, 0x1E, 0x02, 0x1E, 0x02, 0x3E, 0x02, + 0xFF, 0xFF, 0x3F, 0xFF, 0xFC, 0xFF, 0xEC, 0xFF, + 0xED, 0xFE, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, + 0x90, 0x90, 0x00, 0x00, 0x00, 0x00, 0x21, 0x21, + 0x20, 0x21, 0x00, 0x01, 0x00, 0x01, 0x40, 0x41, + 0x8F, 0x7F, 0x1F, 0xFF, 0x1F, 0xFF, 0x3F, 0xDE, + 0x1F, 0xFE, 0x3F, 0xFE, 0x3F, 0xFE, 0x7F, 0xBE, + 0x40, 0x7F, 0x84, 0xFF, 0x11, 0xF1, 0x20, 0xE0, + 0x20, 0xE0, 0x01, 0xC1, 0x01, 0xC1, 0x22, 0xE3, + 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x0E, 0xFF, 0x1F, + 0xDF, 0x3F, 0xFE, 0x3F, 0xFF, 0x3E, 0xFD, 0x1E, + 0x47, 0xC0, 0x27, 0xE0, 0x2F, 0xE8, 0x0F, 0xE9, + 0x0F, 0xE1, 0x0F, 0xE0, 0x3F, 0xF0, 0x3F, 0xF1, + 0xF8, 0x3F, 0xF8, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, + 0xF0, 0x1F, 0xF0, 0x1F, 0xE0, 0x1F, 0xC0, 0x3F, + 0xFC, 0x00, 0xFE, 0xE2, 0x1E, 0x12, 0x1E, 0x12, + 0x3E, 0x22, 0xFC, 0x00, 0xF8, 0x08, 0xF0, 0x10, + 0x03, 0xFF, 0x01, 0xFF, 0xE1, 0xFF, 0xE1, 0xFF, + 0xC1, 0xFF, 0x03, 0xFF, 0x07, 0xFF, 0x0F, 0xFF, + 0x01, 0x11, 0x00, 0x08, 0x00, 0x04, 0x00, 0x02, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x1F, 0x07, 0x0F, 0x03, 0x07, 0x01, 0x03, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x80, 0x40, 0x40, 0x30, 0x30, + 0x0C, 0x0C, 0x03, 0xC3, 0x00, 0x30, 0x00, 0x0C, + 0xFF, 0xFF, 0x7F, 0xFF, 0xBF, 0xFF, 0xCF, 0xFF, + 0xF3, 0xFF, 0x3C, 0xFF, 0x0F, 0x3F, 0x03, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0xC0, 0x3C, 0x3C, 0x03, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0xFF, 0xC3, 0xFF, 0xFC, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xE0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, + 0x15, 0x15, 0x3F, 0x20, 0x2F, 0x20, 0x06, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xEA, 0xE6, 0xDF, 0xC0, 0xDF, 0xE0, 0xF9, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xE6, 0x20, 0x9E, 0x12, 0x0C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xDF, 0x30, 0xED, 0x31, 0xFF, 0x63, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x0E, 0x02, 0x1E, 0x12, 0x0D, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFD, 0x03, 0xED, 0xE1, 0xFE, 0xF1, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x4F, 0x08, 0xE6, 0x20, 0xE7, 0x21, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF7, 0x18, 0xDF, 0x18, 0xDE, 0x18, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xB9, 0xA1, 0x11, 0x01, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x5E, 0x46, 0xFE, 0xC6, 0xFF, 0xC6, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x3F, 0xFF, 0x01, 0xFF, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7E, 0x4E, 0x3F, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xB1, 0x8E, 0xFF, 0x80, 0xFF, 0x80, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xEE, 0x20, 0x8F, 0x08, 0x85, 0x84, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xDF, 0x30, 0xF7, 0x38, 0x7B, 0x7C, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xAE, 0xA2, 0xF8, 0x00, 0xE8, 0x08, 0xC0, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x5D, 0xE1, 0xFF, 0x03, 0xF7, 0x0F, 0x3F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x0E, 0x02, 0x1E, 0x12, 0x1E, 0x12, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFD, 0xF1, 0xED, 0xF1, 0xED, 0xE3, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFB, 0xFB, 0x7F, 0x7F, 0x3F, 0x3F, 0x0C, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x75, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xDF, 0xC1, 0xDF, 0xD0, 0x8F, 0x88, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFE, 0xFF, 0xEF, 0xFF, 0x77, 0xFF, 0xFE, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFA, 0x82, 0xF8, 0x08, 0xE0, 0x00, 0x81, 0x81, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7E, 0xFD, 0xF4, 0xFF, 0xFC, 0xFF, 0x7E, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7D, 0x7D, 0x02, 0x02, 0x02, 0x02, 0xFC, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xFE, 0x01, 0xFF, 0x01, 0xFF, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x1C, 0xFF, 0x00, 0xFF, 0x41, 0x7F, 0x18, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF7, 0x08, 0xFF, 0x00, 0xFF, 0x80, 0xE7, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x5E, 0xC2, 0x9C, 0x80, 0x1C, 0x04, 0x08, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xE1, 0x3F, 0xE3, 0x7F, 0xE3, 0xFF, 0xF7, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0x80, 0x78, 0x08, 0x78, 0x48, 0x10, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0xFF, 0x87, 0xFF, 0x87, 0xFF, 0xEF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0xFF, 0x03, 0x3F, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0x1C, 0x03, 0x03, 0x00, 0xE0, 0x00, 0x1C, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xE3, 0xFF, 0xFC, 0xFF, 0x1F, 0xFF, 0x03, 0x1F, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFC, 0xFC, 0x03, 0x03, 0x00, 0x00, + 0x00, 0xFC, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, + 0x03, 0xFF, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x3F, 0x3F, + 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xC0, 0xFF, + 0xFF, 0xFF, 0x3F, 0xFF, 0x00, 0x3F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, + 0x3F, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, + 0xC0, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0x00, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF +}; diff --git a/Core/graphics/dmg_border.inc b/Core/graphics/dmg_border.inc new file mode 100644 index 0000000..7db0673 --- /dev/null +++ b/Core/graphics/dmg_border.inc @@ -0,0 +1,558 @@ +static const uint16_t palette[] = { + 0x0000, 0x0011, 0x18C6, 0x001A, 0x318C, 0x39CE, 0x5294, 0x5AD6, + 0x739C, 0x45A8, 0x4520, 0x18A5, 0x4A32, 0x2033, 0x20EC, 0x0000, +}; + +static const uint16_t tilemap[] = { + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0001, 0x0003, 0x0004, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x4004, 0x4003, 0x0001, + 0x0001, 0x0006, 0x0007, 0x0007, 0x0007, 0x0008, 0x0009, 0x000A, + 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0010, 0x0011, 0x0012, + 0x0013, 0x0014, 0x0015, 0x000E, 0x0016, 0x0017, 0x0018, 0x0019, + 0x001A, 0x001B, 0x001C, 0x0007, 0x0007, 0x0007, 0x4006, 0x0001, + 0x0001, 0x001D, 0x001E, 0x001E, 0x001E, 0x001F, 0x0020, 0x0021, + 0x0022, 0x0023, 0x0024, 0x0025, 0x4024, 0x0026, 0x0025, 0x0025, + 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, + 0x002F, 0x0030, 0x0031, 0x001E, 0x001E, 0x001E, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0034, 0x0035, 0x4034, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x8034, 0x0036, 0xC034, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0x0037, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0x0038, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0039, 0x003A, 0x0001, + 0x0001, 0x003B, 0x003C, 0x0032, 0x0032, 0xC03C, 0x003D, 0x003D, + 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, + 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, + 0x003D, 0x003D, 0x003E, 0x003F, 0x0040, 0x0041, 0x0001, 0x0001, + 0x0001, 0x0042, 0x0043, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, + 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, + 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, + 0x0044, 0x0044, 0x0045, 0x0046, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, + 0x004E, 0x004F, 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x005B, + 0x005C, 0x005D, 0x005E, 0x005F, 0x0060, 0x0061, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, + 0x0068, 0x0069, 0x006A, 0x006B, 0x0001, 0x006C, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, +}; + +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, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0x01, 0xFE, 0x06, 0xF9, 0x08, 0xF7, + 0x11, 0xEF, 0x22, 0xDB, 0x20, 0xDB, 0x40, 0xB7, + 0xFF, 0x00, 0xFF, 0x00, 0xFE, 0x00, 0xF8, 0x00, + 0xF1, 0x00, 0xE6, 0x04, 0xE4, 0x00, 0xC8, 0x00, + 0x7F, 0x80, 0x80, 0x7F, 0x00, 0xFF, 0x7F, 0xFF, + 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x80, 0x00, 0x00, 0x00, 0x7F, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0xB7, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0xC8, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x03, 0xFF, 0x02, 0xFF, + 0x02, 0xFF, 0x02, 0xFF, 0x02, 0xFF, 0x02, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xC1, 0xDD, 0x00, 0xC9, + 0x14, 0xFF, 0x14, 0xFF, 0x14, 0xFF, 0x00, 0xC9, + 0x00, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x36, 0x22, + 0x14, 0x00, 0x14, 0x00, 0x14, 0x00, 0x36, 0x22, + 0x00, 0xFF, 0x00, 0xFF, 0xC7, 0xDF, 0x01, 0xCF, + 0x11, 0xFF, 0x11, 0xFF, 0x11, 0xFF, 0x01, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x31, 0x20, + 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x31, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0xC2, 0xFF, 0x03, 0xFF, + 0x02, 0xFE, 0x02, 0xFE, 0x02, 0xFF, 0x02, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xC2, 0x00, 0x03, 0x00, + 0x03, 0x01, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xFF, 0x18, 0xFF, + 0x08, 0x4E, 0x08, 0x4E, 0x09, 0x1F, 0x08, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x18, 0x00, + 0xB9, 0x10, 0xB9, 0xA1, 0xE9, 0xA0, 0xEB, 0x41, + 0x00, 0xFF, 0x00, 0xFF, 0x4F, 0xFF, 0x02, 0x1F, + 0x02, 0x4F, 0x02, 0x4F, 0xF2, 0xFF, 0x02, 0xE7, + 0x00, 0x00, 0x00, 0x00, 0x4F, 0x00, 0xE2, 0xA0, + 0xB2, 0xA0, 0xB2, 0x10, 0xF2, 0x00, 0x1A, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0xBC, 0xFD, 0x22, 0xFB, + 0x22, 0xFB, 0x3C, 0xFD, 0x24, 0xFF, 0x26, 0xF9, + 0x00, 0x00, 0x00, 0x00, 0xBE, 0x00, 0x26, 0x00, + 0x26, 0x00, 0x3E, 0x00, 0x24, 0x00, 0x26, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x50, 0xFF, 0x49, 0xEF, + 0x49, 0xF0, 0x46, 0xFF, 0x49, 0xF0, 0x49, 0xEF, + 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x59, 0x00, + 0x4F, 0x06, 0x46, 0x00, 0x4F, 0x06, 0x59, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x88, 0xFF, 0x00, 0x72, + 0x00, 0xF2, 0x05, 0xFF, 0x00, 0xF8, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x88, 0x00, 0x8D, 0x08, + 0x0D, 0x05, 0x05, 0x00, 0x07, 0x05, 0x87, 0x02, + 0x00, 0xFF, 0x00, 0xFF, 0x8A, 0xFF, 0x02, 0x27, + 0x02, 0x27, 0x52, 0xFF, 0x02, 0x8F, 0x02, 0x8F, + 0x00, 0x00, 0x00, 0x00, 0x8A, 0x00, 0xDA, 0x88, + 0xDA, 0x50, 0x52, 0x00, 0x72, 0x50, 0x72, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0xFA, 0xFF, 0x22, 0xFF, + 0x22, 0xFF, 0x23, 0xFF, 0x22, 0xFF, 0x22, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x22, 0x00, + 0x22, 0x00, 0x23, 0x00, 0x22, 0x00, 0x22, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x20, 0xFF, 0x20, 0xFF, + 0x20, 0xFF, 0xE0, 0xFF, 0x20, 0xFF, 0x20, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, + 0x20, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x20, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x33, 0x37, 0x00, 0x77, + 0x80, 0xFF, 0x20, 0x27, 0x08, 0xFF, 0x00, 0x77, + 0x00, 0x00, 0x00, 0x00, 0xFB, 0x40, 0x88, 0x88, + 0x80, 0x00, 0xF8, 0x50, 0x08, 0x00, 0x88, 0x88, + 0x00, 0xFF, 0x00, 0xFF, 0xEF, 0xFF, 0x88, 0xFF, + 0x88, 0xFF, 0x8F, 0xFF, 0x88, 0xFF, 0x88, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xEF, 0x00, 0x88, 0x00, + 0x88, 0x00, 0x8F, 0x00, 0x88, 0x00, 0x88, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xF9, 0xFD, 0x80, 0xF9, + 0x84, 0xFF, 0xF4, 0xFF, 0x84, 0xFF, 0x80, 0xF9, + 0x00, 0x00, 0x00, 0x00, 0xFB, 0x00, 0x86, 0x02, + 0x84, 0x00, 0xF4, 0x00, 0x84, 0x00, 0x86, 0x02, + 0x00, 0xFF, 0x00, 0xFF, 0xC0, 0xDF, 0x00, 0xCF, + 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x00, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x30, 0x20, + 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x30, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0x30, 0x36, 0x00, 0x74, + 0x82, 0xFF, 0x22, 0x27, 0x0A, 0xFF, 0x00, 0x74, + 0x00, 0x00, 0x00, 0x00, 0xF9, 0x40, 0x8B, 0x89, + 0x82, 0x00, 0xFA, 0x50, 0x0A, 0x00, 0x8B, 0x89, + 0x00, 0xFF, 0x00, 0xFF, 0xE2, 0xEF, 0x02, 0xE7, + 0x0A, 0xFF, 0x0A, 0xFF, 0x0A, 0xFF, 0x00, 0xE4, + 0x00, 0x00, 0x00, 0x00, 0xF2, 0x00, 0x1A, 0x10, + 0x0A, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x1B, 0x12, + 0x00, 0xFF, 0x00, 0xFF, 0x14, 0xFF, 0x16, 0xFF, + 0x14, 0xFC, 0x15, 0xFE, 0x14, 0xFF, 0x04, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x16, 0x00, + 0x17, 0x01, 0x15, 0x00, 0x14, 0x00, 0x34, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0x2F, 0xFF, 0x28, 0xFF, + 0x28, 0xFF, 0xA8, 0x7F, 0x28, 0x3F, 0x68, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x28, 0x00, + 0x28, 0x00, 0xA8, 0x00, 0xE8, 0x80, 0x68, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0x3F, + 0x40, 0xFF, 0x40, 0xFF, 0x40, 0xFF, 0x00, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x80, + 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xC0, 0x80, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xC1, 0xDD, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC1, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x02, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x4A, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x0A, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x22, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x82, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x20, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x60, 0x67, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF8, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x8F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x8F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xA2, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xF9, 0xFD, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC0, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x60, 0x66, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF9, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xE0, 0xEE, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC4, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE4, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x2F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0xFF, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, + 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x3C, 0xFF, + 0x7E, 0xFF, 0xE7, 0xE7, 0xFF, 0x7E, 0xFF, 0x7E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x7E, + 0x81, 0xC3, 0x00, 0x99, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x7E, 0xFF, 0x3C, 0xFF, 0x00, 0x7E, 0x81, + 0x3C, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0xC3, 0x81, + 0x7E, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF7, 0x00, 0xF7, 0x00, 0xF7, 0x00, 0xF7, + 0x00, 0xF7, 0x00, 0xED, 0x00, 0xED, 0x00, 0xED, + 0x09, 0x00, 0x09, 0x00, 0x09, 0x00, 0x09, 0x00, + 0x09, 0x00, 0x11, 0x02, 0x11, 0x02, 0x11, 0x02, + 0x00, 0xED, 0x00, 0xDB, 0x00, 0xDB, 0x00, 0xDB, + 0x00, 0xB7, 0x00, 0xB7, 0x00, 0x6F, 0x00, 0x6F, + 0x11, 0x02, 0x23, 0x04, 0x23, 0x04, 0x23, 0x04, + 0x47, 0x08, 0x47, 0x08, 0x8F, 0x10, 0x8F, 0x10, + 0x00, 0xFE, 0x00, 0xFD, 0x00, 0xFB, 0x00, 0xF7, + 0x00, 0xEE, 0x00, 0xDD, 0x00, 0xBB, 0x00, 0x77, + 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x08, 0x00, + 0x10, 0x01, 0x21, 0x02, 0x43, 0x04, 0x87, 0x08, + 0x00, 0xDF, 0x00, 0xBF, 0x00, 0xBF, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x1F, 0x20, 0x3F, 0x40, 0x3F, 0x40, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xB7, + 0x00, 0xB7, 0x00, 0xDB, 0x00, 0xDD, 0x00, 0xEE, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x88, 0x40, + 0x88, 0x40, 0xC4, 0x20, 0xC2, 0x20, 0xE1, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFC, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFC, 0x00, 0xE3, 0x00, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x1C, 0x00, 0xE0, 0x00, + 0x00, 0xFE, 0x00, 0xFD, 0x00, 0xF3, 0x00, 0xEF, + 0x00, 0x1C, 0x00, 0xF3, 0x00, 0xEF, 0x00, 0x1F, + 0x01, 0x00, 0x02, 0x00, 0x0C, 0x00, 0x10, 0x00, + 0xE0, 0x03, 0x03, 0x0C, 0x0F, 0x10, 0x1F, 0xE0, + 0x00, 0xEF, 0x00, 0xDF, 0x00, 0xBF, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x0F, 0x10, 0x1F, 0x20, 0x3F, 0x40, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xF7, 0x00, 0xF9, 0x00, 0xFE, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xF0, 0x08, 0xF8, 0x06, 0xFE, 0x01, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x80, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x7F, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x03, 0x00, 0xFF, 0x00, 0xFC, 0x00, 0x03, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0xFC, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFC, 0x00, 0xE3, 0x00, 0x1F, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x03, 0x03, 0x1C, 0x1F, 0xE0, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x21, 0xDE, 0x00, 0x7F, + 0x0C, 0xF3, 0x19, 0xE0, 0x10, 0xEE, 0x08, 0xF7, + 0xFF, 0x00, 0xFF, 0x00, 0xC0, 0x3F, 0x80, 0xFF, + 0x00, 0xFF, 0x0E, 0xF7, 0x1F, 0xE1, 0x07, 0xF8, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0xBF, + 0x40, 0xBE, 0x80, 0x3F, 0x02, 0xFD, 0x00, 0xFB, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, 0xC0, + 0x7F, 0x81, 0xFE, 0x41, 0xFC, 0x03, 0xFC, 0x07, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xF7, 0x00, 0xFF, + 0x00, 0xFB, 0x04, 0xFB, 0x24, 0xDB, 0x64, 0x9B, + 0xFF, 0x00, 0xFF, 0x00, 0x87, 0x78, 0x07, 0xF8, + 0x07, 0xFC, 0x07, 0xF8, 0x03, 0xFC, 0x43, 0xBC, + 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xDE, 0x20, 0xDF, + 0x20, 0xDF, 0x00, 0xFF, 0x04, 0xFB, 0x04, 0xFB, + 0xFF, 0x00, 0xFF, 0x00, 0xE0, 0x3F, 0xC0, 0x3F, + 0xC0, 0x3F, 0xC0, 0x3F, 0xC0, 0x3F, 0xC0, 0x3F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, 0x00, 0xFF, + 0x00, 0x77, 0x00, 0x7F, 0x80, 0x6F, 0x82, 0x7D, + 0xFF, 0x00, 0xFF, 0x00, 0xFC, 0x07, 0xF8, 0x07, + 0xF8, 0x8F, 0xF0, 0x8F, 0x70, 0x9F, 0x60, 0x9F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, 0x24, 0xCB, + 0x24, 0xDB, 0x20, 0xDF, 0x20, 0xDF, 0x00, 0xDF, + 0xFF, 0x00, 0xFF, 0x00, 0x1C, 0xE7, 0x18, 0xF7, + 0x18, 0xE7, 0x18, 0xE7, 0x38, 0xC7, 0x38, 0xE7, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x02, 0xFC, + 0x7E, 0x81, 0x80, 0x01, 0x80, 0x7F, 0xF8, 0x03, + 0xFF, 0x00, 0xFF, 0x00, 0x01, 0xFE, 0x01, 0xFF, + 0x01, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x07, 0xFC, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xBF, 0x40, 0xBF, + 0x47, 0xB8, 0x08, 0xF0, 0x08, 0xF7, 0x0E, 0xF1, + 0xFF, 0x00, 0xFF, 0x00, 0xC0, 0x7F, 0x80, 0x7F, + 0x80, 0x7F, 0x87, 0x7F, 0x87, 0x78, 0x80, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x40, 0x9F, 0x00, 0xFF, + 0x10, 0xEF, 0x90, 0x6F, 0x10, 0xEB, 0x14, 0xEB, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0xE0, 0x1F, 0xE0, + 0x0E, 0xF1, 0x0C, 0xF3, 0x0C, 0xF7, 0x18, 0xE7, + 0x00, 0xFF, 0x00, 0xFF, 0x20, 0x9F, 0x00, 0xFF, + 0x0C, 0xF3, 0x31, 0xC0, 0x60, 0x9F, 0x40, 0xBF, + 0xFF, 0x00, 0xFF, 0x00, 0xC0, 0x7F, 0x00, 0xFF, + 0x00, 0xFF, 0x1E, 0xEF, 0x3F, 0xC0, 0x7F, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x77, 0x04, 0xDB, + 0x00, 0xFB, 0x10, 0xEF, 0x00, 0xFD, 0x80, 0x77, + 0xFF, 0x00, 0xFF, 0x00, 0x78, 0x8F, 0x38, 0xE7, + 0x1C, 0xE7, 0x0C, 0xF3, 0x0E, 0xF3, 0x0E, 0xF9, + 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, 0x00, 0xFF, + 0x40, 0xB7, 0x00, 0xEF, 0x01, 0xDE, 0x02, 0xFC, + 0xFF, 0x00, 0xFF, 0x00, 0x7C, 0x83, 0x78, 0x87, + 0x38, 0xCF, 0x30, 0xDF, 0x21, 0xFE, 0x03, 0xFD, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xDF, 0x60, 0x9F, + 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0xE0, 0x3F, 0xC0, + 0x7F, 0x80, 0xFF, 0x00, 0xFF, 0x80, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, 0x01, 0xFC, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0x01, 0xFC, 0x03, 0xFC, 0x07, 0xFE, 0x03, + 0x00, 0xFF, 0x40, 0x3F, 0x30, 0x8F, 0x00, 0xF7, + 0x80, 0x7F, 0x30, 0xCF, 0x01, 0xFE, 0x87, 0x78, + 0x01, 0xFE, 0x80, 0xFF, 0xE0, 0x5F, 0xF8, 0x0F, + 0x78, 0x87, 0x00, 0xFF, 0x00, 0xFF, 0x03, 0xFC, + 0x00, 0xFF, 0x08, 0xF7, 0x80, 0x6F, 0x80, 0x7F, + 0x80, 0x5F, 0x87, 0x78, 0x04, 0x7B, 0x08, 0x73, + 0xF8, 0x07, 0xF0, 0x0F, 0x70, 0x9F, 0x60, 0x9F, + 0x60, 0xBF, 0xC3, 0x3C, 0x87, 0xF8, 0x87, 0xFC, + 0xA0, 0x1F, 0x80, 0x7D, 0xE2, 0x1D, 0x02, 0xFD, + 0x02, 0xFD, 0xF0, 0x0F, 0x10, 0xEE, 0x11, 0xEE, + 0x43, 0xFC, 0xE3, 0x1E, 0x03, 0xFC, 0x01, 0xFE, + 0x01, 0xFE, 0xE1, 0x1E, 0xE1, 0x1F, 0xE1, 0x1E, + 0x44, 0xBB, 0x48, 0xB3, 0x48, 0xB7, 0x08, 0xF7, + 0x0A, 0xF5, 0x02, 0xF5, 0x80, 0x77, 0x90, 0x67, + 0x84, 0x7B, 0x84, 0x7F, 0x84, 0x7B, 0x84, 0x7B, + 0x8C, 0x73, 0x8C, 0x7B, 0x0E, 0xF9, 0x0E, 0xF9, + 0x86, 0x59, 0x06, 0xF9, 0x48, 0xB3, 0x08, 0xF7, + 0x10, 0xE7, 0x14, 0xEB, 0x24, 0xCB, 0x20, 0xDF, + 0x60, 0xBF, 0x44, 0xBB, 0x04, 0xFF, 0x0C, 0xF3, + 0x0C, 0xFB, 0x18, 0xE7, 0x18, 0xF7, 0x38, 0xC7, + 0x08, 0xD7, 0x48, 0x97, 0x48, 0xB7, 0x41, 0xBE, + 0x41, 0xBE, 0x01, 0xBE, 0x10, 0xAF, 0x90, 0x2F, + 0x30, 0xEF, 0x30, 0xEF, 0x30, 0xCF, 0x30, 0xCF, + 0x70, 0x8F, 0x70, 0xCF, 0x60, 0xDF, 0x60, 0xDF, + 0x04, 0xFB, 0x04, 0xFB, 0xFC, 0x03, 0x00, 0xFF, + 0x00, 0xFF, 0xF8, 0x02, 0x05, 0xFA, 0x05, 0xFA, + 0x03, 0xFC, 0x03, 0xFC, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x07, 0xFD, 0x02, 0xFD, 0x06, 0xF9, + 0x80, 0x7F, 0x80, 0x7F, 0x0F, 0xF0, 0x10, 0xE7, + 0x10, 0xEE, 0x1E, 0xE1, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x0E, 0xF1, 0x0F, 0xF8, + 0x0F, 0xF1, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x60, 0x8F, 0x00, 0xDF, 0x00, 0xFF, 0x00, 0xEF, + 0x04, 0xEB, 0x20, 0xCF, 0x22, 0xDD, 0xC1, 0x1E, + 0x38, 0xD7, 0x38, 0xE7, 0x18, 0xE7, 0x18, 0xF7, + 0x18, 0xF7, 0x1C, 0xF3, 0x3E, 0xC1, 0x7F, 0xA0, + 0x80, 0x3F, 0x80, 0x7F, 0x80, 0x7F, 0x01, 0xFE, + 0x00, 0xBD, 0x18, 0xE7, 0x00, 0xFF, 0x83, 0x7C, + 0x7F, 0xC0, 0x7F, 0x80, 0x7F, 0x80, 0x7E, 0x81, + 0x7E, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x81, 0x76, 0x80, 0x77, 0x10, 0xE7, 0x10, 0xEF, + 0x10, 0xEF, 0x21, 0xCE, 0x41, 0x9E, 0x81, 0x3E, + 0x0E, 0xF9, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF0, + 0x1F, 0xE0, 0x3E, 0xD1, 0x7E, 0xA1, 0xFE, 0x41, + 0x04, 0xF9, 0x08, 0xF3, 0x18, 0xE7, 0x10, 0xEF, + 0x10, 0xEF, 0x10, 0xEF, 0x00, 0xEF, 0x20, 0xCF, + 0x07, 0xFA, 0x07, 0xFC, 0x0F, 0xF0, 0x0F, 0xF0, + 0x0F, 0xF0, 0x1F, 0xE0, 0x1F, 0xF0, 0x1F, 0xF0, + 0x7C, 0x01, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x82, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x78, 0x87, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x0F, 0xF0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x70, 0x8E, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x01, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC3, 0x18, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x24, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x8F, 0x70, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xF8, 0x03, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x04, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x3E, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0xC1, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xE0, 0x1F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, +}; diff --git a/Core/sgb_animation_logo.inc b/Core/graphics/sgb_animation_logo.inc similarity index 100% rename from Core/sgb_animation_logo.inc rename to Core/graphics/sgb_animation_logo.inc diff --git a/Core/sgb_border.inc b/Core/graphics/sgb_border.inc similarity index 100% rename from Core/sgb_border.inc rename to Core/graphics/sgb_border.inc diff --git a/Core/joypad.c b/Core/joypad.c index 7713cbd..b8d4fdb 100644 --- a/Core/joypad.c +++ b/Core/joypad.c @@ -3,6 +3,8 @@ void GB_update_joyp(GB_gameboy_t *gb) { + if (gb->model & GB_MODEL_NO_SFC_BIT) return; + uint8_t key_selection = 0; uint8_t previous_state = 0; @@ -10,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 : 0; + uint8_t current_player = gb->sgb? (gb->sgb->current_player & (gb->sgb->player_count - 1) & 3) : 0; switch (key_selection) { case 3: if (gb->sgb && gb->sgb->player_count > 1) { @@ -53,19 +55,32 @@ void GB_update_joyp(GB_gameboy_t *gb) break; } + /* Todo: This assumes the keys *always* bounce, which is incorrect when emulating an SGB */ if (previous_state != (gb->io_registers[GB_IO_JOYP] & 0xF)) { - /* The joypad interrupt DOES occur on CGB (Tested on CGB-CPU-06), unlike what some documents say. */ + /* The joypad interrupt DOES occur on CGB (Tested on CGB-E), unlike what some documents say. */ gb->io_registers[GB_IO_IF] |= 0x10; - gb->stopped = false; } gb->io_registers[GB_IO_JOYP] |= 0xC0; } +void GB_icd_set_joyp(GB_gameboy_t *gb, uint8_t value) +{ + uint8_t previous_state = gb->io_registers[GB_IO_JOYP] & 0xF; + gb->io_registers[GB_IO_JOYP] &= 0xF0; + gb->io_registers[GB_IO_JOYP] |= value & 0xF; + + if (previous_state & ~(gb->io_registers[GB_IO_JOYP] & 0xF)) { + gb->io_registers[GB_IO_IF] |= 0x10; + } + gb->io_registers[GB_IO_JOYP] |= 0xC0; +} + void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed) { assert(index >= 0 && index < GB_KEY_MAX); gb->keys[0][index] = pressed; + GB_update_joyp(gb); } void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned player, bool pressed) @@ -73,4 +88,5 @@ void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned play assert(index >= 0 && index < GB_KEY_MAX); assert(player < 4); gb->keys[player][index] = pressed; + GB_update_joyp(gb); } diff --git a/Core/joypad.h b/Core/joypad.h index 768d685..21fad53 100644 --- a/Core/joypad.h +++ b/Core/joypad.h @@ -17,6 +17,7 @@ typedef enum { 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_icd_set_joyp(GB_gameboy_t *gb, uint8_t value); #ifdef GB_INTERNAL void GB_update_joyp(GB_gameboy_t *gb); diff --git a/Core/mbc.c b/Core/mbc.c index d3791a1..ba5055f 100644 --- a/Core/mbc.c +++ b/Core/mbc.c @@ -37,8 +37,8 @@ const GB_cartridge_t GB_cart_defs[256] = { [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) - { GB_HUC3 , GB_STANDARD_MBC, true , true , false, false}, // FEh HuC3 (Todo: Mapper support only) - { GB_HUC1 , GB_STANDARD_MBC, true , true , false, false}, // FFh HuC1+RAM+BATTERY (Todo: No IR bindings) + { GB_HUC3 , GB_STANDARD_MBC, true , true , true, false}, // FEh HuC3 + { GB_HUC1 , GB_STANDARD_MBC, true , true , false, false}, // FFh HuC1+RAM+BATTERY }; void GB_update_mbc_mappings(GB_gameboy_t *gb) @@ -86,6 +86,10 @@ void GB_update_mbc_mappings(GB_gameboy_t *gb) case GB_MBC3: gb->mbc_rom_bank = gb->mbc3.rom_bank; gb->mbc_ram_bank = gb->mbc3.ram_bank; + if (!gb->is_mbc30) { + gb->mbc_rom_bank &= 0x7F; + gb->mbc_ram_bank &= 0x3; + } if (gb->mbc_rom_bank == 0) { gb->mbc_rom_bank = 1; } @@ -128,10 +132,13 @@ void GB_configure_cart(GB_gameboy_t *gb) gb->mbc_ram_size = 0x200; } else { - static const int ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000}; + static const unsigned ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000}; gb->mbc_ram_size = ram_sizes[gb->rom[0x149]]; } - gb->mbc_ram = malloc(gb->mbc_ram_size); + + if (gb->mbc_ram_size) { + gb->mbc_ram = malloc(gb->mbc_ram_size); + } /* Todo: Some games assume unintialized MBC RAM is 0xFF. It this true for all cartridges types? */ memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); @@ -147,6 +154,13 @@ void GB_configure_cart(GB_gameboy_t *gb) } } + /* Detect MBC30 */ + if (gb->cartridge_type->mbc_type == GB_MBC3) { + if (gb->rom_size > 0x200000 || gb->mbc_ram_size > 0x8000) { + gb->is_mbc30 = true; + } + } + /* Set MBC5's bank to 1 correctly */ if (gb->cartridge_type->mbc_type == GB_MBC5) { gb->mbc5.rom_bank_low = 1; diff --git a/Core/mbc.h b/Core/mbc.h index 7e9b47f..6a23300 100644 --- a/Core/mbc.h +++ b/Core/mbc.h @@ -10,7 +10,7 @@ typedef struct { GB_MBC2, GB_MBC3, GB_MBC5, - GB_HUC1, /* Todo: HUC1 features are not emulated. Should be unified with the CGB IR sensor API. */ + GB_HUC1, GB_HUC3, } mbc_type; enum { diff --git a/Core/memory.c b/Core/memory.c index c15ae42..3f924bc 100644 --- a/Core/memory.c +++ b/Core/memory.c @@ -113,6 +113,11 @@ static bool is_addr_in_dma_use(GB_gameboy_t *gb, uint16_t addr) return bus_for_addr(gb, addr) == bus_for_addr(gb, gb->dma_current_src); } +static bool effective_ir_input(GB_gameboy_t *gb) +{ + return gb->infrared_input || (gb->io_registers[GB_IO_RP] & 1) || gb->cart_ir; +} + static uint8_t read_rom(GB_gameboy_t *gb, uint16_t addr) { if (addr < 0x100 && !gb->boot_rom_finished) { @@ -126,13 +131,13 @@ static uint8_t read_rom(GB_gameboy_t *gb, uint16_t addr) if (!gb->rom_size) { return 0xFF; } - unsigned int effective_address = (addr & 0x3FFF) + gb->mbc_rom0_bank * 0x4000; + unsigned effective_address = (addr & 0x3FFF) + gb->mbc_rom0_bank * 0x4000; return gb->rom[effective_address & (gb->rom_size - 1)]; } static uint8_t read_mbc_rom(GB_gameboy_t *gb, uint16_t addr) { - unsigned int effective_address = (addr & 0x3FFF) + gb->mbc_rom_bank * 0x4000; + unsigned effective_address = (addr & 0x3FFF) + gb->mbc_rom_bank * 0x4000; return gb->rom[effective_address & (gb->rom_size - 1)]; } @@ -141,16 +146,56 @@ static uint8_t read_vram(GB_gameboy_t *gb, uint16_t addr) if (gb->vram_read_blocked) { return 0xFF; } + 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; + } + } return gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000]; } static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) { + if (gb->cartridge_type->mbc_type == GB_HUC3) { + switch (gb->huc3_mode) { + case 0xC: // RTC read + if (gb->huc3_access_flags == 0x2) { + return 1; + } + 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? + default: + 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 + break; + } + } + if ((!gb->mbc_ram_enable || !gb->mbc_ram_size) && gb->cartridge_type->mbc_subtype != GB_CAMERA && - gb->cartridge_type->mbc_type != GB_HUC1) return 0xFF; + gb->cartridge_type->mbc_type != GB_HUC1 && + gb->cartridge_type->mbc_type != GB_HUC3) { + return 0xFF; + } - if (gb->cartridge_type->has_rtc && gb->mbc_ram_bank >= 8 && gb->mbc_ram_bank <= 0xC) { + if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) { + return 0xc0 | effective_ir_input(gb); + } + + if (gb->cartridge_type->has_rtc && gb->cartridge_type->mbc_type != GB_HUC3 && + gb->mbc_ram_bank >= 8 && gb->mbc_ram_bank <= 0xC) { /* RTC read */ gb->rtc_latched.high |= ~0xC1; /* Not all bytes in RTC high are used. */ return gb->rtc_latched.data[gb->mbc_ram_bank - 8]; @@ -273,8 +318,11 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) case GB_MODEL_DMG_B: case GB_MODEL_SGB_NTSC: case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: case GB_MODEL_SGB2: - ; + case GB_MODEL_SGB2_NO_SFC: + break; } } @@ -292,21 +340,22 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) return gb->io_registers[GB_IO_TAC] | 0xF8; case GB_IO_STAT: return gb->io_registers[GB_IO_STAT] | 0x80; - case GB_IO_DMG_EMULATION_INDICATION: - if (!gb->cgb_mode) { + case GB_IO_OPRI: + if (!GB_is_cgb(gb)) { return 0xFF; } - return gb->io_registers[GB_IO_DMG_EMULATION_INDICATION] | 0xFE; + return gb->io_registers[GB_IO_OPRI] | 0xFE; case GB_IO_PCM_12: if (!GB_is_cgb(gb)) return 0xFF; - 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); + 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: if (!GB_is_cgb(gb)) return 0xFF; - 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); + 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_timing_sync(gb); case GB_IO_TMA: case GB_IO_LCDC: case GB_IO_SCY: @@ -375,9 +424,8 @@ 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. */ - bool read_value = gb->infrared_input || (gb->io_registers[GB_IO_RP] & 1); uint8_t ret = (gb->io_registers[GB_IO_RP] & 0xC1) | 0x3C; - if ((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && read_value) { + if ((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && effective_ir_input(gb)) { ret |= 2; } return ret; @@ -418,6 +466,11 @@ static GB_read_function_t * const read_map[] = read_ram, read_high_memory, /* EXXX FXXX */ }; +void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t callback) +{ + gb->read_memory_callback = callback; +} + uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr) { if (gb->n_watchpoints) { @@ -426,7 +479,12 @@ uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr) if (is_addr_in_dma_use(gb, addr)) { addr = gb->dma_current_src; } - return read_map[addr >> 12](gb, addr); + uint8_t data = read_map[addr >> 12](gb, addr); + GB_apply_cheat(gb, addr, &data); + if (gb->read_memory_callback) { + data = gb->read_memory_callback(gb, addr, data); + } + return data; } static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) @@ -442,9 +500,9 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } break; case GB_MBC2: - switch (addr & 0xF000) { - case 0x0000: case 0x1000: if (!(addr & 0x100)) gb->mbc_ram_enable = (value & 0xF) == 0xA; break; - case 0x2000: case 0x3000: if ( addr & 0x100) gb->mbc2.rom_bank = value; break; + switch (addr & 0x4100) { + case 0x0000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x0100: gb->mbc2.rom_bank = value; break; } break; case GB_MBC3: @@ -469,9 +527,6 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (gb->cartridge_type->has_rumble) { if (!!(value & 8) != gb->rumble_state) { gb->rumble_state = !gb->rumble_state; - if (gb->rumble_callback) { - gb->rumble_callback(gb, gb->rumble_state); - } } value &= 7; } @@ -482,7 +537,7 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) break; case GB_HUC1: switch (addr & 0xF000) { - case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x0000: case 0x1000: gb->huc1.ir_mode = (value & 0xF) == 0xE; break; case 0x2000: case 0x3000: gb->huc1.bank_low = value; break; case 0x4000: case 0x5000: gb->huc1.bank_high = value; break; case 0x6000: case 0x7000: gb->huc1.mode = value; break; @@ -490,7 +545,10 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) break; case GB_HUC3: switch (addr & 0xF000) { - case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x0000: case 0x1000: + 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; } @@ -505,17 +563,135 @@ static void write_vram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) //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; } +static bool huc3_write(GB_gameboy_t *gb, uint8_t value) +{ + 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; + } + 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->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)); + } + 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 >= 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 { + // 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++; + } + break; + case 4: + 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; + break; + case 6: + gb->huc3_access_flags = (value & 0xF); + break; + + default: + break; + } + + return true; + case 0xD: // RTC status + // 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->infrared_callback) { + gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); + } + gb->cycles_since_ir_change = 0; + } + return true; + } + case 0xC: + return true; + default: + return false; + case 0: // Disabled + case 0xA: // RAM + return false; + } +} + static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { + if (gb->cartridge_type->mbc_type == GB_HUC3) { + if (huc3_write(gb, value)) return; + } + if (gb->camera_registers_mapped) { GB_camera_write_register(gb, addr, value); return; } - if (!gb->mbc_ram_enable || !gb->mbc_ram_size) return; + if ((!gb->mbc_ram_enable || !gb->mbc_ram_size) + && 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->infrared_callback) { + gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); + } + gb->cycles_since_ir_change = 0; + } + return; + } if (gb->cartridge_type->has_rtc && gb->mbc_ram_bank >= 8 && gb->mbc_ram_bank <= 0xC) { gb->rtc_latched.data[gb->mbc_ram_bank - 8] = gb->rtc_real.data[gb->mbc_ram_bank - 8] = value; @@ -583,7 +759,10 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_MODEL_DMG_B: case GB_MODEL_SGB_NTSC: case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: case GB_MODEL_CGB_E: case GB_MODEL_AGB: break; @@ -628,24 +807,34 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (addr < 0xFF80) { /* Hardware registers */ switch (addr & 0xFF) { + case GB_IO_WY: + if (value == gb->current_line) { + gb->wy_triggered = true; + } case GB_IO_WX: - GB_window_related_write(gb, addr & 0xFF, value); - break; case GB_IO_IF: case GB_IO_SCX: case GB_IO_SCY: case GB_IO_BGP: case GB_IO_OBP0: case GB_IO_OBP1: - case GB_IO_WY: case GB_IO_SB: - case GB_IO_DMG_EMULATION_INDICATION: case GB_IO_UNKNOWN2: case GB_IO_UNKNOWN3: case GB_IO_UNKNOWN4: case GB_IO_UNKNOWN5: gb->io_registers[addr & 0xFF] = value; return; + case GB_IO_OPRI: + if ((!gb->boot_rom_finished || (gb->io_registers[GB_IO_KEY0] & 8)) && GB_is_cgb(gb)) { + gb->io_registers[addr & 0xFF] = value; + gb->object_priority = (value & 1) ? GB_OBJECT_PRIORITY_X : GB_OBJECT_PRIORITY_INDEX; + } + else if (gb->cgb_mode) { + gb->io_registers[addr & 0xFF] = value; + + } + return; case GB_IO_LYC: /* TODO: Probably completely wrong in double speed mode */ @@ -662,7 +851,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) /* These are the states when LY changes, let the display routine call GB_STAT_update for use so it correctly handles T-cycle accurate LYC writes */ if (!GB_is_cgb(gb) || ( - gb->display_state != 6 && + gb->display_state != 35 && gb->display_state != 26 && gb->display_state != 15 && gb->display_state != 16)) { @@ -714,8 +903,19 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_timing_sync(gb); GB_lcd_off(gb); } - /* Writing to LCDC might enable to disable the window, so we write it via GB_window_related_write */ - GB_window_related_write(gb, addr & 0xFF, value); + /* Handle disabling objects while already fetching an object */ + if ((gb->io_registers[GB_IO_LCDC] & 2) && !(value & 2)) { + if (gb->during_object_fetch) { + gb->cycles_for_line += gb->display_cycles; + gb->display_cycles = 0; + gb->object_fetch_aborted = true; + } + } + gb->io_registers[GB_IO_LCDC] = value; + if (!(value & 0x20)) { + gb->wx_triggered = false; + gb->wx166_glitch = false; + } return; case GB_IO_STAT: @@ -736,18 +936,25 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; case GB_IO_JOYP: - GB_sgb_write(gb, value); - gb->io_registers[GB_IO_JOYP] = value & 0xF0; - GB_update_joyp(gb); + if (gb->joyp_write_callback) { + gb->joyp_write_callback(gb, value); + GB_update_joyp(gb); + } + else if ((gb->io_registers[GB_IO_JOYP] & 0x30) != (value & 0x30)) { + GB_sgb_write(gb, value); + gb->io_registers[GB_IO_JOYP] = (value & 0xF0) | (gb->io_registers[GB_IO_JOYP] & 0x0F); + GB_update_joyp(gb); + } return; - case GB_IO_BIOS: + case GB_IO_BANK: gb->boot_rom_finished = true; return; - case GB_IO_DMG_EMULATION: + case GB_IO_KEY0: if (GB_is_cgb(gb) && !gb->boot_rom_finished) { gb->cgb_mode = !(value & 0xC); /* The real "contents" of this register aren't quite known yet. */ + gb->io_registers[GB_IO_KEY0] = value; } return; @@ -889,13 +1096,15 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (!GB_is_cgb(gb)) { return; } - if ((value & 1) != (gb->io_registers[GB_IO_RP] & 1)) { - if (gb->infrared_callback) { - gb->infrared_callback(gb, value & 1, gb->cycles_since_ir_change); - gb->cycles_since_ir_change = 0; - } - } + 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->infrared_callback) { + gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); + } + gb->cycles_since_ir_change = 0; + } return; } diff --git a/Core/memory.h b/Core/memory.h index 03d636d..f0d0390 100644 --- a/Core/memory.h +++ b/Core/memory.h @@ -3,6 +3,9 @@ #include "gb_struct_def.h" #include +typedef uint8_t (*GB_read_memory_callback_t)(GB_gameboy_t *gb, uint16_t addr, uint8_t data); +void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t callback); + uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr); void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value); #ifdef GB_INTERNAL diff --git a/Core/printer.c b/Core/printer.c index add1f86..7b47ace 100644 --- a/Core/printer.c +++ b/Core/printer.c @@ -31,8 +31,8 @@ static void handle_command(GB_gameboy_t *gb) image[i] = colors[(palette >> (gb->printer.image[i] * 2)) & 3]; } - if (gb->printer.callback) { - gb->printer.callback(gb, image, gb->printer.image_offset / 160, + if (gb->printer_callback) { + gb->printer_callback(gb, image, gb->printer.image_offset / 160, gb->printer.command_data[1] >> 4, gb->printer.command_data[1] & 7, gb->printer.command_data[3] & 0x7F); } @@ -212,5 +212,5 @@ void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback) memset(&gb->printer, 0, sizeof(gb->printer)); GB_set_serial_transfer_bit_start_callback(gb, serial_start); GB_set_serial_transfer_bit_end_callback(gb, serial_end); - gb->printer.callback = callback; + gb->printer_callback = callback; } diff --git a/Core/printer.h b/Core/printer.h index 7cf179e..71f919a 100644 --- a/Core/printer.h +++ b/Core/printer.h @@ -48,7 +48,8 @@ typedef struct uint8_t image[160 * 200]; uint16_t image_offset; - GB_print_image_callback_t callback; + /* TODO: Delete me. */ + uint64_t padding; uint8_t compression_run_lenth; bool compression_run_is_compressed; diff --git a/Core/random.c b/Core/random.c new file mode 100644 index 0000000..cc4d4d3 --- /dev/null +++ b/Core/random.c @@ -0,0 +1,38 @@ +#include "random.h" +#include + +static uint64_t seed; +static bool enabled = true; + +uint8_t GB_random(void) +{ + if (!enabled) return 0; + + seed *= 0x27BB2EE687B0B0FDL; + seed += 0xB504F32D; + return seed >> 56; +} + +uint32_t GB_random32(void) +{ + GB_random(); + return seed >> 32; +} + +void GB_random_seed(uint64_t new_seed) +{ + seed = new_seed; +} + +void GB_random_set_enabled(bool enable) +{ + enabled = enable; +} + +static void __attribute__((constructor)) init_seed(void) +{ + seed = time(NULL); + for (unsigned i = 64; i--;) { + GB_random(); + } +} diff --git a/Core/random.h b/Core/random.h new file mode 100644 index 0000000..8ab0e50 --- /dev/null +++ b/Core/random.h @@ -0,0 +1,12 @@ +#ifndef random_h +#define random_h + +#include +#include + +uint8_t GB_random(void); +uint32_t GB_random32(void); +void GB_random_seed(uint64_t seed); +void GB_random_set_enabled(bool enable); + +#endif /* random_h */ diff --git a/Core/rumble.c b/Core/rumble.c new file mode 100644 index 0000000..8cbe20d --- /dev/null +++ b/Core/rumble.c @@ -0,0 +1,53 @@ +#include "rumble.h" +#include "gb.h" + +void GB_set_rumble_mode(GB_gameboy_t *gb, GB_rumble_mode_t mode) +{ + gb->rumble_mode = mode; + if (gb->rumble_callback) { + gb->rumble_callback(gb, 0); + } +} + +void GB_handle_rumble(GB_gameboy_t *gb) +{ + if (gb->rumble_callback) { + if (gb->rumble_mode == GB_RUMBLE_DISABLED) { + return; + } + if (gb->cartridge_type->has_rumble) { + 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; + } + } + else if (gb->rumble_mode == GB_RUMBLE_ALL_GAMES) { + 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; + + 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)) { + 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); + ch1_rumble = MAX(ch1_rumble, 0.0); + } + + if (!gb->apu.is_active[GB_NOISE]) { + ch4_rumble = 0; + } + + if (!gb->apu.is_active[GB_SQUARE_1]) { + ch1_rumble = 0; + } + + gb->rumble_callback(gb, MIN(MAX(ch1_rumble / 2 + ch4_rumble, 0.0), 1.0)); + } + } +} diff --git a/Core/rumble.h b/Core/rumble.h new file mode 100644 index 0000000..eae9f37 --- /dev/null +++ b/Core/rumble.h @@ -0,0 +1,17 @@ +#ifndef rumble_h +#define rumble_h + +#include "gb_struct_def.h" + +typedef enum { + GB_RUMBLE_DISABLED, + GB_RUMBLE_CARTRIDGE_ONLY, + GB_RUMBLE_ALL_GAMES +} GB_rumble_mode_t; + +#ifdef 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); + +#endif /* rumble_h */ diff --git a/Core/save_state.c b/Core/save_state.c index 26c9b52..9ef6ae3 100644 --- a/Core/save_state.c +++ b/Core/save_state.c @@ -36,11 +36,10 @@ int GB_save_state(GB_gameboy_t *gb, const char *path) if (!DUMP_SECTION(gb, f, rtc )) goto error; if (!DUMP_SECTION(gb, f, video )) goto error; - if (GB_is_sgb(gb)) { + 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; } @@ -73,7 +72,7 @@ size_t GB_get_save_state_size(GB_gameboy_t *gb) + GB_SECTION_SIZE(apu ) + sizeof(uint32_t) + GB_SECTION_SIZE(rtc ) + sizeof(uint32_t) + GB_SECTION_SIZE(video ) + sizeof(uint32_t) - + (GB_is_sgb(gb)? sizeof(*gb->sgb) + sizeof(uint32_t) : 0) + + (GB_is_hle_sgb(gb)? sizeof(*gb->sgb) + sizeof(uint32_t) : 0) + gb->mbc_ram_size + gb->ram_size + gb->vram_size; @@ -105,7 +104,7 @@ void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer) DUMP_SECTION(gb, buffer, rtc ); DUMP_SECTION(gb, buffer, video ); - if (GB_is_sgb(gb)) { + if (GB_is_hle_sgb(gb)) { buffer_dump_section(&buffer, gb->sgb, sizeof(*gb->sgb)); } @@ -116,13 +115,21 @@ void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer) } /* Best-effort read function for maximum future compatibility. */ -static bool read_section(FILE *f, void *dest, uint32_t size) +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; @@ -139,11 +146,21 @@ static bool read_section(FILE *f, void *dest, uint32_t size) } #undef DUMP_SECTION -static bool verify_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save) +static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save) { - 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 (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; + } } if (gb->version != save->version) { @@ -156,25 +173,53 @@ static bool verify_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save) return false; } - if (gb->ram_size != save->ram_size) { - GB_log(gb, "The save state has non-matching RAM size. Try changing the emulated model.\n"); - return false; - } - if (gb->vram_size != save->vram_size) { GB_log(gb, "The save state has non-matching VRAM size. Try changing the emulated model.\n"); return false; } - if (GB_is_sgb(gb) != GB_is_sgb(save)) { - GB_log(gb, "The save state is %sfor a Super Game Boy. Try changing the emulated model.\n", GB_is_sgb(save)? "" : "not "); + if (GB_is_hle_sgb(gb) != GB_is_hle_sgb(save)) { + GB_log(gb, "The save state is %sfor a Super Game Boy. Try changing the emulated model.\n", GB_is_hle_sgb(save)? "" : "not "); return false; } + 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; + } + } + return true; } -#define READ_SECTION(gb, f, section) read_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section)) +static void sanitize_state(GB_gameboy_t *gb) +{ + for (unsigned i = 0; i < 32; i++) { + GB_palette_changed(gb, false, i * 2); + GB_palette_changed(gb, true, i * 2); + } + + gb->bg_fifo.read_end &= 0xF; + gb->bg_fifo.write_end &= 0xF; + gb->oam_fifo.read_end &= 0xF; + gb->oam_fifo.write_end &= 0xF; + 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; + } +} + +#define READ_SECTION(gb, f, section) read_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section), fix_broken_windows_saves) int GB_load_state(GB_gameboy_t *gb, const char *path) { @@ -182,6 +227,8 @@ int GB_load_state(GB_gameboy_t *gb, const char *path) /* Every unread value should be kept the same. */ memcpy(&save, gb, sizeof(save)); + /* ...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) { @@ -189,7 +236,18 @@ int GB_load_state(GB_gameboy_t *gb, const char *path) 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 (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; + 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, f, core_state)) goto error; if (!READ_SECTION(&save, f, dma )) goto error; if (!READ_SECTION(&save, f, mbc )) goto error; @@ -199,13 +257,13 @@ int GB_load_state(GB_gameboy_t *gb, const char *path) if (!READ_SECTION(&save, f, rtc )) goto error; if (!READ_SECTION(&save, f, video )) goto error; - if (!verify_state_compatibility(gb, &save)) { + if (!verify_and_update_state_compatibility(gb, &save)) { errno = -1; goto error; } - if (GB_is_sgb(gb)) { - if (!read_section(f, gb->sgb, sizeof(*gb->sgb))) goto error; + if (GB_is_hle_sgb(gb)) { + if (!read_section(f, gb->sgb, sizeof(*gb->sgb), false)) goto error; } memset(gb->mbc_ram + save.mbc_ram_size, 0xFF, gb->mbc_ram_size - save.mbc_ram_size); @@ -218,28 +276,22 @@ int GB_load_state(GB_gameboy_t *gb, const char *path) fclose(f); return 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); if (fread(gb->vram, 1, gb->vram_size, f) != gb->vram_size) { fclose(f); return EIO; } + size_t orig_ram_size = gb->ram_size; memcpy(gb, &save, sizeof(save)); + gb->ram_size = orig_ram_size; + errno = 0; - if (gb->cartridge_type->has_rumble && gb->rumble_callback) { - gb->rumble_callback(gb, gb->rumble_state); - } - - for (unsigned i = 0; i < 32; i++) { - GB_palette_changed(gb, false, i * 2); - GB_palette_changed(gb, true, i * 2); - } - - gb->bg_fifo.read_end &= 0xF; - gb->bg_fifo.write_end &= 0xF; - gb->oam_fifo.read_end &= 0xF; - gb->oam_fifo.write_end &= 0xF; + sanitize_state(gb); error: fclose(f); @@ -262,13 +314,23 @@ static size_t buffer_read(void *dest, size_t length, const uint8_t **buffer, siz return length; } -static bool buffer_read_section(const uint8_t **buffer, size_t *buffer_length, void *dest, uint32_t size) +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; @@ -285,15 +347,27 @@ static bool buffer_read_section(const uint8_t **buffer, size_t *buffer_length, v return true; } -#define READ_SECTION(gb, buffer, length, section) buffer_read_section(&buffer, &length, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section)) +#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; @@ -302,13 +376,14 @@ int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t le 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_state_compatibility(gb, &save)) { + if (!verify_and_update_state_compatibility(gb, &save)) { return -1; } - if (GB_is_sgb(gb)) { - if (!buffer_read_section(&buffer, &length, gb->sgb, sizeof(*gb->sgb))) 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); @@ -320,25 +395,17 @@ int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t le return -1; } - if (buffer_read(gb->vram,gb->vram_size, &buffer, &length) != gb->vram_size) { + 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)); - if (gb->cartridge_type->has_rumble && gb->rumble_callback) { - gb->rumble_callback(gb, gb->rumble_state); - } - - for (unsigned i = 0; i < 32; i++) { - GB_palette_changed(gb, false, i * 2); - GB_palette_changed(gb, true, i * 2); - } - - gb->bg_fifo.read_end &= 0xF; - gb->bg_fifo.write_end &= 0xF; - gb->oam_fifo.read_end &= 0xF; - gb->oam_fifo.write_end &= 0xF; + sanitize_state(gb); return 0; } diff --git a/Core/save_state.h b/Core/save_state.h index 546ac2d..fcb9135 100644 --- a/Core/save_state.h +++ b/Core/save_state.h @@ -5,7 +5,7 @@ #define GB_PADDING(type, old_usage) type old_usage##__do_not_use -#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) struct {} name##_section_start; __VA_ARGS__; struct {} name##_section_end +#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) union {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)) diff --git a/Core/sgb.c b/Core/sgb.c index 9e7e382..c77b0db 100644 --- a/Core/sgb.c +++ b/Core/sgb.c @@ -1,5 +1,11 @@ #include "gb.h" +#include "random.h" #include +#include + +#ifndef M_PI + #define M_PI 3.14159265358979323846 +#endif #define INTRO_ANIMATION_LENGTH 200 @@ -11,6 +17,7 @@ enum { ATTR_BLK = 0x04, ATTR_LIN = 0x05, ATTR_DIV = 0x06, + ATTR_CHR = 0x07, PAL_SET = 0x0A, PAL_TRN = 0x0B, DATA_SND = 0x0F, @@ -66,6 +73,75 @@ static inline void load_attribute_file(GB_gameboy_t *gb, unsigned file_index) } } +static const uint16_t built_in_palettes[] = +{ + 0x67BF, 0x265B, 0x10B5, 0x2866, + 0x637B, 0x3AD9, 0x0956, 0x0000, + 0x7F1F, 0x2A7D, 0x30F3, 0x4CE7, + 0x57FF, 0x2618, 0x001F, 0x006A, + 0x5B7F, 0x3F0F, 0x222D, 0x10EB, + 0x7FBB, 0x2A3C, 0x0015, 0x0900, + 0x2800, 0x7680, 0x01EF, 0x2FFF, + 0x73BF, 0x46FF, 0x0110, 0x0066, + 0x533E, 0x2638, 0x01E5, 0x0000, + 0x7FFF, 0x2BBF, 0x00DF, 0x2C0A, + 0x7F1F, 0x463D, 0x74CF, 0x4CA5, + 0x53FF, 0x03E0, 0x00DF, 0x2800, + 0x433F, 0x72D2, 0x3045, 0x0822, + 0x7FFA, 0x2A5F, 0x0014, 0x0003, + 0x1EED, 0x215C, 0x42FC, 0x0060, + 0x7FFF, 0x5EF7, 0x39CE, 0x0000, + 0x4F5F, 0x630E, 0x159F, 0x3126, + 0x637B, 0x121C, 0x0140, 0x0840, + 0x66BC, 0x3FFF, 0x7EE0, 0x2C84, + 0x5FFE, 0x3EBC, 0x0321, 0x0000, + 0x63FF, 0x36DC, 0x11F6, 0x392A, + 0x65EF, 0x7DBF, 0x035F, 0x2108, + 0x2B6C, 0x7FFF, 0x1CD9, 0x0007, + 0x53FC, 0x1F2F, 0x0E29, 0x0061, + 0x36BE, 0x7EAF, 0x681A, 0x3C00, + 0x7BBE, 0x329D, 0x1DE8, 0x0423, + 0x739F, 0x6A9B, 0x7293, 0x0001, + 0x5FFF, 0x6732, 0x3DA9, 0x2481, + 0x577F, 0x3EBC, 0x456F, 0x1880, + 0x6B57, 0x6E1B, 0x5010, 0x0007, + 0x0F96, 0x2C97, 0x0045, 0x3200, + 0x67FF, 0x2F17, 0x2230, 0x1548, +}; + +static const struct { + char name[16]; + unsigned palette_index; +} palette_assignments[] = +{ + {"ZELDA", 5}, + {"SUPER MARIOLAND", 6}, + {"MARIOLAND2", 0x14}, + {"SUPERMARIOLAND3", 2}, + {"KIRBY DREAM LAND", 0xB}, + {"HOSHINOKA-BI", 0xB}, + {"KIRBY'S PINBALL", 3}, + {"YOSSY NO TAMAGO", 0xC}, + {"MARIO & YOSHI", 0xC}, + {"YOSSY NO COOKIE", 4}, + {"YOSHI'S COOKIE", 4}, + {"DR.MARIO", 0x12}, + {"TETRIS", 0x11}, + {"YAKUMAN", 0x13}, + {"METROID2", 0x1F}, + {"KAERUNOTAMENI", 9}, + {"GOLF", 0x18}, + {"ALLEY WAY", 0x16}, + {"BASEBALL", 0xF}, + {"TENNIS", 0x17}, + {"F1RACE", 0x1E}, + {"KID ICARUS", 0xE}, + {"QIX", 0x19}, + {"SOLARSTRIKER", 7}, + {"X", 0x1C}, + {"GBWARS", 0x15}, +}; + static void command_ready(GB_gameboy_t *gb) { /* SGB header commands are used to send the contents of the header to the SNES CPU. @@ -75,6 +151,8 @@ static void command_ready(GB_gameboy_t *gb) 0xE content bytes. The last command, FB, is padded with zeros, so information past the header is not sent. */ if ((gb->sgb->command[0] & 0xF1) == 0xF1) { + if (gb->boot_rom_finished) return; + uint8_t checksum = 0; for (unsigned i = 2; i < 0x10; i++) { checksum += gb->sgb->command[i]; @@ -84,19 +162,31 @@ static void command_ready(GB_gameboy_t *gb) gb->sgb->disable_commands = true; return; } - if (gb->sgb->command[0] == 0xf9) { - if (gb->sgb->command[0xc] != 3) { // SGB Flag - gb->sgb->disable_commands = true; - } + unsigned index = (gb->sgb->command[0] >> 1) & 7; + if (index > 5) { + return; } - else if (gb->sgb->command[0] == 0xfb) { - if (gb->sgb->command[0x3] != 0x33) { // Old licensee code + memcpy(&gb->sgb->received_header[index * 14], &gb->sgb->command[2], 14); + if (gb->sgb->command[0] == 0xfb) { + if (gb->sgb->received_header[0x42] != 3 || gb->sgb->received_header[0x47] != 0x33) { 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]; + break; + } + } } } return; } + /* Ignore malformed commands (0 length)*/ + if ((gb->sgb->command[0] & 7) == 0) return; + switch (gb->sgb->command[0] >> 3) { case PAL01: pal_command(gb, 0, 1); @@ -157,7 +247,7 @@ static void command_ready(GB_gameboy_t *gb) gb->sgb->attribute_map[x + 20 * y] = inside_palette; } } - else if(middle) { + else if (middle) { gb->sgb->attribute_map[x + 20 * y] = middle_palette; } } @@ -165,6 +255,52 @@ static void command_ready(GB_gameboy_t *gb) } break; } + case ATTR_CHR: { + struct __attribute__((packed)) { + uint8_t x, y; + uint16_t length; + uint8_t direction; + uint8_t data[]; + } *command = (void *)(gb->sgb->command + 1); + + uint16_t count = command->length; +#ifdef GB_BIG_ENDIAN + count = __builtin_bswap16(count); +#endif + uint8_t x = command->x; + uint8_t y = command->y; + if (x >= 20 || y >= 18 || (count + 3) / 4 > sizeof(gb->sgb->command) - sizeof(*command) - 1) { + /* TODO: Verify with the SFC BIOS */ + break; + } + + for (unsigned i = 0; i < count; i++) { + uint8_t palette = (command->data[i / 4] >> (((~i) & 3) << 1)) & 3; + gb->sgb->attribute_map[x + 20 * y] = palette; + if (command->direction) { + y++; + if (y == 18) { + x++; + y = 0; + if (x == 20) { + x = 0; + } + } + } + else { + x++; + if (x == 20) { + y++; + x = 0; + if (y == 18) { + y = 0; + } + } + } + } + + break; + } case ATTR_LIN: { struct { uint8_t count; @@ -248,8 +384,15 @@ static void command_ready(GB_gameboy_t *gb) // Not supported, but used by almost all SGB games for hot patching, so let's mute the warning for this break; case MLT_REQ: - gb->sgb->player_count = (uint8_t[]){1, 2, 1, 4}[gb->sgb->command[1] & 3]; - gb->sgb->current_player = gb->sgb->player_count - 1; + 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->mlt_lock = true; break; case CHR_TRN: gb->sgb->vram_transfer_countdown = 2; @@ -289,23 +432,33 @@ static void command_ready(GB_gameboy_t *gb) } void GB_sgb_write(GB_gameboy_t *gb, uint8_t value) -{ +{ if (!GB_is_sgb(gb)) return; + if (!GB_is_hle_sgb(gb)) { + /* Notify via callback */ + return; + } if (gb->sgb->disable_commands) return; - if (gb->sgb->command_write_index >= sizeof(gb->sgb->command) * 8) 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; + } + switch ((value >> 4) & 3) { case 3: gb->sgb->ready_for_pulse = true; - /* TODO: This is the logic used by BGB which *should* work for most/all games, but a proper test ROM is needed */ - if (gb->sgb->player_count > 1 && (gb->io_registers[GB_IO_JOYP] & 0x30) == 0x10) { + if ((gb->sgb->player_count & 1) == 0 && !gb->sgb->mlt_lock) { gb->sgb->current_player++; - gb->sgb->current_player &= gb->sgb->player_count - 1; + gb->sgb->current_player &= 3; + gb->sgb->mlt_lock = true; } break; @@ -366,22 +519,9 @@ void GB_sgb_write(GB_gameboy_t *gb, uint8_t value) } } -static inline uint8_t scale_channel(uint8_t x) -{ - return (x << 3) | (x >> 2); -} - static uint32_t convert_rgb15(GB_gameboy_t *gb, uint16_t color) { - uint8_t r = (color) & 0x1F; - uint8_t g = (color >> 5) & 0x1F; - uint8_t b = (color >> 10) & 0x1F; - - r = scale_channel(r); - g = scale_channel(g); - b = scale_channel(b); - - return gb->rgb_encode_callback(gb, r, g, b); + return GB_convert_rgb15(gb, color, false); } static uint32_t convert_rgb15_with_fade(GB_gameboy_t *gb, uint16_t color, uint8_t fade) @@ -394,18 +534,19 @@ static uint32_t convert_rgb15_with_fade(GB_gameboy_t *gb, uint16_t color, uint8_ if (g >= 0x20) g = 0; if (b >= 0x20) b = 0; - r = scale_channel(r); - g = scale_channel(g); - b = scale_channel(b); + color = r | (g << 5) | (b << 10); - return gb->rgb_encode_callback(gb, r, g, b); + return GB_convert_rgb15(gb, color, false); } #include static void render_boot_animation (GB_gameboy_t *gb) { -#include "sgb_animation_logo.inc" - uint32_t *output = &gb->screen[48 + 40 * 256]; +#include "graphics/sgb_animation_logo.inc" + uint32_t *output = gb->screen; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 48 + 40 * 256; + } uint8_t *input = animation_logo; unsigned fade_blue = 0; unsigned fade_red = 0; @@ -453,22 +594,21 @@ static void render_boot_animation (GB_gameboy_t *gb) input++; } } - output += 256 - 160; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } } } +static void render_jingle(GB_gameboy_t *gb, size_t count); void GB_sgb_render(GB_gameboy_t *gb) { - if (gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) gb->sgb->intro_animation++; - - if (!gb->screen || !gb->rgb_encode_callback) return; - - if (gb->sgb->mask_mode != MASK_FREEZE) { - memcpy(gb->sgb->effective_screen_buffer, - gb->sgb->screen_buffer, - sizeof(gb->sgb->effective_screen_buffer)); + if (gb->apu_output.sample_rate) { + 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->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) { @@ -530,16 +670,27 @@ void GB_sgb_render(GB_gameboy_t *gb) } } + if (!gb->screen || !gb->rgb_encode_callback || gb->disable_rendering) return; + uint32_t colors[4 * 4]; for (unsigned i = 0; i < 4 * 4; i++) { colors[i] = convert_rgb15(gb, gb->sgb->effective_palettes[i]); } + if (gb->sgb->mask_mode != MASK_FREEZE) { + memcpy(gb->sgb->effective_screen_buffer, + gb->sgb->screen_buffer, + sizeof(gb->sgb->effective_screen_buffer)); + } + if (gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) { render_boot_animation(gb); } else { - uint32_t *output = &gb->screen[48 + 40 * 256]; + uint32_t *output = gb->screen; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 48 + 40 * 256; + } uint8_t *input = gb->sgb->effective_screen_buffer; switch ((mask_mode_t) gb->sgb->mask_mode) { case MASK_DISABLED: @@ -549,7 +700,9 @@ void GB_sgb_render(GB_gameboy_t *gb) uint8_t palette = gb->sgb->attribute_map[x / 8 + y / 8 * 20] & 3; *(output++) = colors[(*(input++) & 3) + palette * 4]; } - output += 256 - 160; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } } break; } @@ -560,7 +713,9 @@ void GB_sgb_render(GB_gameboy_t *gb) for (unsigned x = 0; x < 160; x++) { *(output++) = black; } - output += 256 - 160; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } } break; } @@ -570,7 +725,9 @@ void GB_sgb_render(GB_gameboy_t *gb) for (unsigned x = 0; x < 160; x++) { *(output++) = colors[0]; } - output += 256 - 160; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } } break; } @@ -607,6 +764,9 @@ void GB_sgb_render(GB_gameboy_t *gb) if (tile_x >= 6 && tile_x < 26 && tile_y >= 5 && tile_y < 23) { gb_area = true; } + 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; @@ -614,12 +774,19 @@ void GB_sgb_render(GB_gameboy_t *gb) for (unsigned y = 0; y < 8; y++) { 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; - if (color == 0) { - if (gb_area) continue; - gb->screen[tile_x * 8 + x + (tile_y * 8 + y) * 0x100] = colors[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; } else { - gb->screen[tile_x * 8 + x + (tile_y * 8 + y) * 0x100] = border_colors[color + palette * 16]; + output += tile_x * 8 + x + (tile_y * 8 + y) * 256; + } + if (color == 0) { + if (gb_area) continue; + *output = colors[0]; + } + else { + *output = border_colors[color + palette * 16]; } } } @@ -630,12 +797,12 @@ void GB_sgb_render(GB_gameboy_t *gb) void GB_sgb_load_default_data(GB_gameboy_t *gb) { -#include "sgb_border.inc" +#include "graphics/sgb_border.inc" memcpy(gb->sgb->border.map, tilemap, sizeof(tilemap)); memcpy(gb->sgb->border.palette, palette, sizeof(palette)); - /* Expend tileset */ + /* 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++) { @@ -658,10 +825,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] = 0x639E; - gb->sgb->effective_palettes[1] = 0x263A; - gb->sgb->effective_palettes[2] = 0x10D4; - gb->sgb->effective_palettes[3] = 0x2866; + 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]; } static double fm_synth(double phase) @@ -682,10 +849,10 @@ static double fm_sweep(double phase) } static double random_double(void) { - return ((random() % 0x10001) - 0x8000) / (double) 0x8000; + return ((signed)(GB_random32() % 0x10001) - 0x8000) / (double) 0x8000; } -bool GB_sgb_render_jingle(GB_gameboy_t *gb, GB_sample_t *dest, size_t count) +static void render_jingle(GB_gameboy_t *gb, size_t count) { const double frequencies[7] = { 466.16, // Bb4 @@ -697,15 +864,17 @@ bool GB_sgb_render_jingle(GB_gameboy_t *gb, GB_sample_t *dest, size_t count) 1567.98, // G6 }; + assert(gb->apu_output.sample_callback); + if (gb->sgb->intro_animation < 0) { + GB_sample_t sample = {0, 0}; for (unsigned i = 0; i < count; i++) { - dest->left = dest->right = 0; - dest++; + gb->apu_output.sample_callback(gb, &sample); } - return true; + return; } - if (gb->sgb->intro_animation >= INTRO_ANIMATION_LENGTH) return false; + if (gb->sgb->intro_animation >= 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; @@ -714,6 +883,7 @@ bool GB_sgb_render_jingle(GB_gameboy_t *gb, GB_sample_t *dest, size_t count) sweep_cutoff_ratio = 1; } + GB_sample_t stereo; for (unsigned i = 0; i < count; i++) { double sample = 0; for (signed f = 0; f < 7 && f < jingle_stage; f++) { @@ -734,10 +904,10 @@ bool GB_sgb_render_jingle(GB_gameboy_t *gb, GB_sample_t *dest, size_t count) sample += gb->sgb_intro_sweep_previous_sample * pow((120 - gb->sgb->intro_animation) / 120.0, 2) * 0.8; } - dest->left = dest->right = sample * 0x7000; - dest++; + stereo.left = stereo.right = sample * 0x7000; + gb->apu_output.sample_callback(gb, &stereo); } - return true; + return; } diff --git a/Core/sgb.h b/Core/sgb.h index 7e36198..aae9f75 100644 --- a/Core/sgb.h +++ b/Core/sgb.h @@ -5,6 +5,16 @@ #include typedef struct GB_sgb_s GB_sgb_t; +typedef struct { + uint8_t tiles[0x100 * 8 * 8]; /* High nibble not used*/ + union { + struct { + uint16_t map[32 * 32]; + uint16_t palette[16 * 4]; + }; + uint16_t raw_data[0x440]; + }; +} GB_sgb_border_t; #ifdef GB_INTERNAL struct GB_sgb_s { @@ -29,16 +39,7 @@ struct GB_sgb_s { uint8_t vram_transfer_countdown, transfer_dest; /* Border */ - struct { - uint8_t tiles[0x100 * 8 * 8]; /* High nibble not used*/ - union { - struct { - uint16_t map[32 * 32]; - uint16_t palette[16 * 4]; - }; - uint16_t raw_data[0x440]; - }; - } border, pending_border; + GB_sgb_border_t border, pending_border; uint8_t border_animation; /* Colorization */ @@ -49,12 +50,17 @@ struct GB_sgb_s { /* 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); -bool GB_sgb_render_jingle(GB_gameboy_t *gb, GB_sample_t *dest, size_t count); #endif diff --git a/Core/sm83_cpu.c b/Core/sm83_cpu.c index fdf357c..3b3eceb 100644 --- a/Core/sm83_cpu.c +++ b/Core/sm83_cpu.c @@ -18,6 +18,9 @@ typedef enum { GB_CONFLICT_STAT_DMG, GB_CONFLICT_PALETTE_DMG, GB_CONFLICT_PALETTE_CGB, + GB_CONFLICT_DMG_LCDC, + GB_CONFLICT_SGB_LCDC, + GB_CONFLICT_WX, } GB_conflict_t; /* Todo: How does double speed mode affect these? */ @@ -29,25 +32,42 @@ static const GB_conflict_t cgb_conflict_map[0x80] = { [GB_IO_OBP0] = GB_CONFLICT_PALETTE_CGB, [GB_IO_OBP1] = GB_CONFLICT_PALETTE_CGB, - /* Todo: most values not verified, and probably differ between revisions */ }; +/* Todo: verify on an MGB */ static const GB_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_READ_NEW, + [GB_IO_LCDC] = GB_CONFLICT_DMG_LCDC, [GB_IO_SCY] = GB_CONFLICT_READ_NEW, [GB_IO_STAT] = GB_CONFLICT_STAT_DMG, - /* Todo: these are GB_CONFLICT_READ_NEW on MGB/SGB2 */ [GB_IO_BGP] = GB_CONFLICT_PALETTE_DMG, [GB_IO_OBP0] = GB_CONFLICT_PALETTE_DMG, [GB_IO_OBP1] = GB_CONFLICT_PALETTE_DMG, - + [GB_IO_WY] = GB_CONFLICT_READ_OLD, + [GB_IO_WX] = GB_CONFLICT_WX, + + /* 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] = { + [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, + [GB_IO_LYC] = GB_CONFLICT_READ_OLD, + [GB_IO_LCDC] = GB_CONFLICT_SGB_LCDC, + [GB_IO_SCY] = GB_CONFLICT_READ_NEW, + [GB_IO_STAT] = GB_CONFLICT_STAT_DMG, + + [GB_IO_BGP] = GB_CONFLICT_READ_NEW, + [GB_IO_OBP0] = GB_CONFLICT_READ_NEW, + [GB_IO_OBP1] = GB_CONFLICT_READ_NEW, + [GB_IO_WY] = GB_CONFLICT_READ_OLD, + [GB_IO_WX] = GB_CONFLICT_WX, + /* Todo: these were not verified at all */ - [GB_IO_WY] = GB_CONFLICT_READ_NEW, - [GB_IO_WX] = GB_CONFLICT_READ_NEW, [GB_IO_SCX] = GB_CONFLICT_READ_NEW, }; @@ -92,7 +112,17 @@ 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; if ((addr & 0xFF80) == 0xFF00) { - conflict = (GB_is_cgb(gb)? cgb_conflict_map : dmg_conflict_map)[addr & 0x7F]; + const GB_conflict_t *map = NULL; + if (GB_is_cgb(gb)) { + map = cgb_conflict_map; + } + else if (GB_is_sgb(gb)) { + map = sgb_conflict_map; + } + else { + map = dmg_conflict_map; + } + conflict = map[addr & 0x7F]; } switch (conflict) { case GB_CONFLICT_READ_OLD: @@ -164,6 +194,53 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->pending_cycles = 6; return; } + + 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. + Hacks ahead. + */ + + + + 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)) { + old_value &= ~2; + } + + GB_write_memory(gb, addr, old_value | (value & 1)); + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 5; + return; + } + + case GB_CONFLICT_SGB_LCDC: { + /* Simplified version of the above */ + + uint8_t old_value = GB_read_memory(gb, addr); + GB_advance_cycles(gb, gb->pending_cycles - 2); + /* Hack to force aborting object fetch */ + GB_write_memory(gb, addr, value); + GB_write_memory(gb, addr, old_value); + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 5; + return; + } + + case GB_CONFLICT_WX: + GB_advance_cycles(gb, gb->pending_cycles); + GB_write_memory(gb, addr, value); + gb->wx_just_changed = true; + GB_advance_cycles(gb, 1); + gb->wx_just_changed = false; + gb->pending_cycles = 3; + return; } } @@ -208,6 +285,26 @@ static void nop(GB_gameboy_t *gb, uint8_t opcode) { } +static void enter_stop_mode(GB_gameboy_t *gb) +{ + gb->stopped = true; + gb->oam_ppu_blocked = !gb->oam_read_blocked; + gb->vram_ppu_blocked = !gb->vram_read_blocked; + gb->cgb_palettes_ppu_blocked = !gb->cgb_palettes_blocked; +} + +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; +} + static void stop(GB_gameboy_t *gb, uint8_t opcode) { if (gb->io_registers[GB_IO_KEY1] & 0x1) { @@ -224,9 +321,8 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) gb->cgb_double_speed ^= true; gb->io_registers[GB_IO_KEY1] = 0; - for (unsigned i = 0x800; i--;) { - GB_advance_cycles(gb, 0x40); - } + enter_stop_mode(gb); + leave_stop_mode(gb); if (!needs_alignment) { GB_advance_cycles(gb, 0x4); @@ -234,7 +330,16 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) } else { - gb->stopped = true; + GB_timing_sync(gb); + if ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF) { + /* HW Bug? When STOP is executed while a button is down, the CPU halts forever + yet the other hardware keeps running. */ + gb->interrupt_enable = 0; + gb->halted = true; + } + else { + enter_stop_mode(gb); + } } /* Todo: is PC being actually read? */ @@ -281,7 +386,7 @@ 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_SUBSTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->registers[GB_REGISTER_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; @@ -297,7 +402,7 @@ static void dec_hr(GB_gameboy_t *gb, uint8_t opcode) 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; if ((gb->registers[register_id] & 0x0F00) == 0xF00) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; @@ -347,7 +452,7 @@ static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode) 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); + cycle_write(gb, addr + 1, gb->registers[GB_REGISTER_SP] >> 8); } static void add_hl_rr(GB_gameboy_t *gb, uint8_t opcode) @@ -359,14 +464,14 @@ static void add_hl_rr(GB_gameboy_t *gb, uint8_t opcode) register_id = (opcode >> 4) + 1; rr = gb->registers[register_id]; gb->registers[GB_REGISTER_HL] = hl + rr; - gb->registers[GB_REGISTER_AF] &= ~(GB_SUBSTRACT_FLAG | GB_CARRY_FLAG | GB_HALF_CARRY_FLAG); + gb->registers[GB_REGISTER_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; } - if ( ((unsigned long) hl + (unsigned long) rr) & 0x10000) { + if ( ((unsigned) hl + (unsigned) rr) & 0x10000) { gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; } } @@ -395,7 +500,7 @@ 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_SUBSTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->registers[GB_REGISTER_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; @@ -415,7 +520,7 @@ static void dec_lr(GB_gameboy_t *gb, uint8_t opcode) 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; if ((gb->registers[register_id] & 0x0F) == 0xF) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; @@ -496,7 +601,7 @@ static void daa(GB_gameboy_t *gb, uint8_t opcode) gb->registers[GB_REGISTER_AF] &= ~(0xFF00 | GB_ZERO_FLAG); - if (gb->registers[GB_REGISTER_AF] & GB_SUBSTRACT_FLAG) { + if (gb->registers[GB_REGISTER_AF] & GB_SUBTRACT_FLAG) { if (gb->registers[GB_REGISTER_AF] & GB_HALF_CARRY_FLAG) { result = (result - 0x06) & 0xFF; } @@ -530,19 +635,19 @@ static void daa(GB_gameboy_t *gb, uint8_t opcode) 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_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_SUBSTRACT_FLAG); + gb->registers[GB_REGISTER_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_SUBSTRACT_FLAG); + gb->registers[GB_REGISTER_AF] &= ~(GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG); } static void ld_dhli_a(GB_gameboy_t *gb, uint8_t opcode) @@ -573,7 +678,7 @@ static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode) value = cycle_read(gb, gb->registers[GB_REGISTER_HL]) + 1; cycle_write(gb, gb->registers[GB_REGISTER_HL], value); - gb->registers[GB_REGISTER_AF] &= ~(GB_SUBSTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); if ((value & 0x0F) == 0) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } @@ -590,7 +695,7 @@ static void dec_dhl(GB_gameboy_t *gb, uint8_t opcode) cycle_write(gb, gb->registers[GB_REGISTER_HL], value); gb->registers[GB_REGISTER_AF] &= ~( GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); - gb->registers[GB_REGISTER_AF] |= GB_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; if ((value & 0x0F) == 0x0F) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } @@ -606,7 +711,7 @@ static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode) cycle_write(gb, gb->registers[GB_REGISTER_HL], data); } -uint8_t get_src_value(GB_gameboy_t *gb, uint8_t opcode) +static uint8_t get_src_value(GB_gameboy_t *gb, uint8_t opcode) { uint8_t src_register_id; uint8_t src_low; @@ -684,6 +789,13 @@ LD_X_Y(l,b) LD_X_Y(l,c) LD_X_Y(l,d) LD_X_Y(l,e) LD_X_Y(l,h) LD_X_DHL LD_DHL_Y(b) LD_DHL_Y(c) LD_DHL_Y(d) LD_DHL_Y(e) LD_DHL_Y(h) LD_DHL_Y(l) LD_DHL_Y(a) LD_X_Y(a,b) LD_X_Y(a,c) LD_X_Y(a,d) LD_X_Y(a,e) LD_X_Y(a,h) LD_X_Y(a,l) LD_X_DHL(a) +// fire the debugger if software breakpoints are enabled +static void ld_b_b(GB_gameboy_t *gb, uint8_t opcode) +{ + if (gb->has_software_breakpoints) { + gb->debug_stopped = true; + } +} static void add_a_r(GB_gameboy_t *gb, uint8_t opcode) { @@ -697,7 +809,7 @@ static void add_a_r(GB_gameboy_t *gb, uint8_t opcode) if ((a & 0xF) + (value & 0xF) > 0x0F) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } - if (((unsigned long) a) + ((unsigned long) value) > 0xFF) { + if (((unsigned) a) + ((unsigned) value) > 0xFF) { gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; } } @@ -716,7 +828,7 @@ static void adc_a_r(GB_gameboy_t *gb, uint8_t opcode) if ((a & 0xF) + (value & 0xF) + carry > 0x0F) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } - if (((unsigned long) a) + ((unsigned long) value) + carry > 0xFF) { + if (((unsigned) a) + ((unsigned) value) + carry > 0xFF) { gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; } } @@ -726,7 +838,7 @@ 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG; if (a == value) { gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; } @@ -744,7 +856,7 @@ static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode) 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; if ((uint8_t) (a - value - carry) == 0) { gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; @@ -752,7 +864,7 @@ static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode) if ((a & 0xF) < (value & 0xF) + carry) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } - if (((unsigned long) a) - ((unsigned long) value) - carry > 0xFF) { + if (((unsigned) a) - ((unsigned) value) - carry > 0xFF) { gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; } } @@ -796,7 +908,7 @@ static void cp_a_r(GB_gameboy_t *gb, uint8_t opcode) 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; if (a == value) { gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; } @@ -812,10 +924,7 @@ static void halt(GB_gameboy_t *gb, uint8_t opcode) { assert(gb->pending_cycles == 4); gb->pending_cycles = 0; - GB_advance_cycles(gb, 1); - GB_advance_cycles(gb, 1); - GB_advance_cycles(gb, 1); - GB_advance_cycles(gb, 1); + GB_advance_cycles(gb, 4); gb->halted = true; /* Despite what some online documentations say, the HALT bug also happens on a CGB, in both CGB and DMG modes. */ @@ -896,7 +1005,7 @@ static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode) if ((a & 0xF) + (value & 0xF) > 0x0F) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } - if (((unsigned long) a) + ((unsigned long) value) > 0xFF) { + if (((unsigned) a) + ((unsigned) value) > 0xFF) { gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; } } @@ -915,7 +1024,7 @@ static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode) if ((a & 0xF) + (value & 0xF) + carry > 0x0F) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } - if (((unsigned long) a) + ((unsigned long) value) + carry > 0xFF) { + if (((unsigned) a) + ((unsigned) value) + carry > 0xFF) { gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; } } @@ -925,7 +1034,7 @@ 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG; if (a == value) { gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; } @@ -943,7 +1052,7 @@ static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode) 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; if ((uint8_t) (a - value - carry) == 0) { gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; @@ -951,7 +1060,7 @@ static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode) if ((a & 0xF) < (value & 0xF) + carry) { gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; } - if (((unsigned long) a) - ((unsigned long) value) - carry > 0xFF) { + if (((unsigned) a) - ((unsigned) value) - carry > 0xFF) { gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; } } @@ -995,7 +1104,7 @@ static void cp_a_d8(GB_gameboy_t *gb, uint8_t opcode) 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_SUBSTRACT_FLAG; + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; if (a == value) { gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; } @@ -1117,7 +1226,7 @@ static void ld_a_da16(GB_gameboy_t *gb, uint8_t opcode) uint16_t addr; gb->registers[GB_REGISTER_AF] &= 0xFF; addr = cycle_read_inc_oam_bug(gb, gb->pc++); - addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8 ; + addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; gb->registers[GB_REGISTER_AF] |= cycle_read(gb, addr) << 8; } @@ -1305,10 +1414,10 @@ static void bit_r(GB_gameboy_t *gb, uint8_t opcode) } } else if ((opcode & 0xC0) == 0x80) { /* res */ - set_src_value(gb, opcode, value & ~bit) ; + set_src_value(gb, opcode, value & ~bit); } else if ((opcode & 0xC0) == 0xC0) { /* set */ - set_src_value(gb, opcode, value | bit) ; + set_src_value(gb, opcode, value | bit); } } @@ -1357,7 +1466,7 @@ static GB_opcode_t *opcodes[256] = { jr_cc_r8, add_hl_rr, ld_a_dhli, dec_rr, inc_lr, dec_lr, ld_lr_d8, cpl, jr_cc_r8, ld_rr_d16, ld_dhld_a, inc_rr, inc_dhl, dec_dhl, ld_dhl_d8, scf, /* 3X */ jr_cc_r8, add_hl_rr, ld_a_dhld, dec_rr, inc_hr, dec_hr, ld_hr_d8, ccf, - nop, ld_b_c, ld_b_d, ld_b_e, ld_b_h, ld_b_l, ld_b_dhl, ld_b_a, /* 4X */ + ld_b_b, ld_b_c, ld_b_d, ld_b_e, ld_b_h, ld_b_l, ld_b_dhl, ld_b_a, /* 4X */ ld_c_b, nop, ld_c_d, ld_c_e, ld_c_h, ld_c_l, ld_c_dhl, ld_c_a, ld_d_b, ld_d_c, nop, ld_d_e, ld_d_h, ld_d_l, ld_d_dhl, ld_d_a, /* 5X */ ld_e_b, ld_e_c, ld_e_d, nop, ld_e_h, ld_e_l, ld_e_dhl, ld_e_a, @@ -1389,10 +1498,19 @@ void GB_cpu_run(GB_gameboy_t *gb) return; } if (gb->stopped) { - GB_advance_cycles(gb, 64); + GB_timing_sync(gb); + GB_advance_cycles(gb, 4); + if ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF) { + leave_stop_mode(gb); + GB_advance_cycles(gb, 8); + } return; } + if ((gb->interrupt_enable & 0x10) && (gb->ime || gb->halted)) { + GB_timing_sync(gb); + } + if (gb->halted && !GB_is_cgb(gb) && !gb->just_halted) { GB_advance_cycles(gb, 2); } @@ -1404,19 +1522,19 @@ void GB_cpu_run(GB_gameboy_t *gb) } gb->just_halted = false; - bool effecitve_ime = gb->ime; + bool effective_ime = gb->ime; if (gb->ime_toggle) { gb->ime = !gb->ime; gb->ime_toggle = false; } /* Wake up from HALT mode without calling interrupt code. */ - if (gb->halted && !effecitve_ime && interrupt_queue) { + if (gb->halted && !effective_ime && interrupt_queue) { gb->halted = false; } /* Call interrupt */ - else if (effecitve_ime && interrupt_queue) { + else if (effective_ime && interrupt_queue) { gb->halted = false; uint16_t call_addr = gb->pc; @@ -1453,7 +1571,7 @@ void GB_cpu_run(GB_gameboy_t *gb) GB_debugger_call_hook(gb, call_addr); } /* Run mode */ - else if(!gb->halted) { + else if (!gb->halted) { gb->last_opcode_read = cycle_read_inc_oam_bug(gb, gb->pc++); if (gb->halt_bug) { gb->pc--; @@ -1462,10 +1580,11 @@ void GB_cpu_run(GB_gameboy_t *gb) opcodes[gb->last_opcode_read](gb, gb->last_opcode_read); } + flush_pending_cycles(gb); + if (gb->hdma_starting) { gb->hdma_starting = false; gb->hdma_on = true; gb->hdma_cycles = -8; } - flush_pending_cycles(gb); } diff --git a/Core/sm83_disassembler.c b/Core/sm83_disassembler.c index 96aec00..7dacd9e 100644 --- a/Core/sm83_disassembler.c +++ b/Core/sm83_disassembler.c @@ -97,7 +97,8 @@ static void rla(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) GB_log(gb, "RLA\n"); } -static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc){ +static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ uint16_t addr; (*pc)++; addr = GB_read_memory(gb, (*pc)++); diff --git a/Core/symbol_hash.c b/Core/symbol_hash.c index 208e72d..75a7837 100644 --- a/Core/symbol_hash.c +++ b/Core/symbol_hash.c @@ -28,8 +28,6 @@ GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const c { size_t index = GB_map_find_symbol_index(map, addr); - if (index < map->n_symbols && map->symbols[index].addr == addr) return NULL; - 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])); map->symbols[index].addr = addr; @@ -71,9 +69,9 @@ void GB_map_free(GB_symbol_map_t *map) free(map); } -static int hash_name(const char *name) +static unsigned hash_name(const char *name) { - int r = 0; + unsigned r = 0; while (*name) { r <<= 1; if (r & 0x400) { @@ -87,7 +85,7 @@ static int hash_name(const char *name) void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *bank_symbol) { - int hash = hash_name(bank_symbol->name); + unsigned hash = hash_name(bank_symbol->name); GB_symbol_t *symbol = malloc(sizeof(*symbol)); symbol->name = bank_symbol->name; symbol->addr = bank_symbol->addr; @@ -98,7 +96,7 @@ void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name) { - int hash = hash_name(name); + unsigned hash = hash_name(name); GB_symbol_t *symbol = map->buckets[hash]; while (symbol) { diff --git a/Core/timing.c b/Core/timing.c index 8affd86..17983bc 100644 --- a/Core/timing.c +++ b/Core/timing.c @@ -3,14 +3,14 @@ #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0500 #endif -#include +#include #else #include #endif -static const unsigned int GB_TAC_TRIGGER_BITS[] = {512, 8, 32, 128}; +static const unsigned GB_TAC_TRIGGER_BITS[] = {512, 8, 32, 128}; -#ifndef DISABLE_TIMEKEEPING +#ifndef GB_DISABLE_TIMEKEEPING static int64_t get_nanoseconds(void) { #ifndef _WIN32 @@ -59,7 +59,7 @@ void GB_timing_sync(GB_gameboy_t *gb) return; } /* Prevent syncing if not enough time has passed.*/ - if (gb->cycles_since_last_sync < LCDC_PERIOD / 4) return; + if (gb->cycles_since_last_sync < LCDC_PERIOD / 3) 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(); @@ -73,6 +73,9 @@ void GB_timing_sync(GB_gameboy_t *gb) } gb->cycles_since_last_sync = 0; + if (gb->update_input_hint_callback) { + gb->update_input_hint_callback(gb); + } } #else @@ -136,6 +139,11 @@ static void GB_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value) static void GB_timers_run(GB_gameboy_t *gb, uint8_t cycles) { + if (gb->stopped) { + gb->apu.apu_cycles += 4 << !gb->cgb_double_speed; + return; + } + GB_STATE_MACHINE(gb, div, cycles, 1) { GB_STATE(gb, div, 1); GB_STATE(gb, div, 2); @@ -206,12 +214,15 @@ static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) } void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) -{ +{ + 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); - advance_serial(gb, cycles); + if (!gb->stopped) { + advance_serial(gb, cycles); // TODO: Verify what happens in STOP mode + } gb->debugger_ticks += cycles; @@ -227,8 +238,18 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) gb->cycles_since_input_ir_change += cycles; gb->cycles_since_last_sync += cycles; gb->cycles_since_run += cycles; - GB_dma_run(gb); - GB_hdma_run(gb); + + if (gb->rumble_state) { + gb->rumble_on_cycles++; + } + else { + gb->rumble_off_cycles++; + } + + if (!gb->stopped) { // TODO: Verify what happens in STOP mode + GB_dma_run(gb); + GB_hdma_run(gb); + } GB_apu_run(gb); GB_display_run(gb, cycles); GB_ir_run(gb); @@ -244,8 +265,8 @@ 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 int old_clocks = GB_TAC_TRIGGER_BITS[old_tac & 3]; - unsigned int new_clocks = GB_TAC_TRIGGER_BITS[new_tac & 3]; + unsigned old_clocks = GB_TAC_TRIGGER_BITS[old_tac & 3]; + unsigned new_clocks = GB_TAC_TRIGGER_BITS[new_tac & 3]; /* The bit used for overflow testing must have been 1 */ if (gb->div_counter & old_clocks) { @@ -258,23 +279,30 @@ void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac) void GB_rtc_run(GB_gameboy_t *gb) { + if (gb->cartridge_type->mbc_type == GB_HUC3) { + time_t current_time = time(NULL); + while (gb->last_rtc_second / 60 < current_time / 60) { + gb->last_rtc_second += 60; + gb->huc3_minutes++; + if (gb->huc3_minutes == 60 * 24) { + gb->huc3_days++; + gb->huc3_minutes = 0; + } + } + return; + } if ((gb->rtc_real.high & 0x40) == 0) { /* is timer running? */ time_t current_time = time(NULL); while (gb->last_rtc_second < current_time) { gb->last_rtc_second++; - if (++gb->rtc_real.seconds == 60) - { + if (++gb->rtc_real.seconds == 60) { gb->rtc_real.seconds = 0; - if (++gb->rtc_real.minutes == 60) - { + if (++gb->rtc_real.minutes == 60) { gb->rtc_real.minutes = 0; - if (++gb->rtc_real.hours == 24) - { + 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*/ - { + 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 02ca54c..d4fa07f 100644 --- a/Core/timing.h +++ b/Core/timing.h @@ -15,7 +15,6 @@ enum { GB_TIMA_RELOADED = 2 }; -#define GB_HALT_VALUE (0xFFFF) #define GB_SLEEP(gb, unit, state, cycles) do {\ (gb)->unit##_cycles -= (cycles) * __state_machine_divisor; \ @@ -26,12 +25,10 @@ enum { }\ } while (0) -#define GB_HALT(gb, unit) (gb)->unit##_cycles = GB_HALT_VALUE - #define GB_STATE_MACHINE(gb, unit, cycles, divisor) \ static const int __state_machine_divisor = divisor;\ (gb)->unit##_cycles += cycles; \ -if ((gb)->unit##_cycles <= 0 || (gb)->unit##_cycles == GB_HALT_VALUE) {\ +if ((gb)->unit##_cycles <= 0) {\ return;\ }\ switch ((gb)->unit##_state) diff --git a/HexFiend/HFFunctions.h b/HexFiend/HFFunctions.h index 3d3586d..aaab107 100644 --- a/HexFiend/HFFunctions.h +++ b/HexFiend/HFFunctions.h @@ -4,7 +4,7 @@ #import #define HFDEFAULT_FONT (@"Monaco") -#define HFDEFAULT_FONTSIZE ((CGFloat)10.) +#define HFDEFAULT_FONTSIZE ((CGFloat)11.) #define HFZeroRange (HFRange){0, 0} diff --git a/HexFiend/HFLineCountingRepresenter.m b/HexFiend/HFLineCountingRepresenter.m index d6b0560..4a81fc8 100644 --- a/HexFiend/HFLineCountingRepresenter.m +++ b/HexFiend/HFLineCountingRepresenter.m @@ -57,8 +57,8 @@ static CGFloat maximumDigitAdvanceForFont(NSFont *font) { interiorShadowEdge = NSMaxXEdge; _borderedEdges = (1 << NSMaxXEdge); - _borderColor = [[NSColor darkGrayColor] retain]; - _backgroundColor = [[NSColor colorWithCalibratedWhite:(CGFloat).87 alpha:1] retain]; + _borderColor = [[NSColor controlShadowColor] retain]; + _backgroundColor = [[NSColor windowBackgroundColor] retain]; } return self; } @@ -82,9 +82,9 @@ static CGFloat maximumDigitAdvanceForFont(NSFont *font) { lineNumberFormat = (HFLineNumberFormat)[coder decodeInt64ForKey:@"HFLineNumberFormat"]; _borderedEdges = [coder decodeObjectForKey:@"HFBorderedEdges"] ? (NSInteger)[coder decodeInt64ForKey:@"HFBorderedEdges"] : 0; - _borderColor = [[coder decodeObjectForKey:@"HFBorderColor"] ?: [NSColor darkGrayColor] retain]; - _backgroundColor = [[coder decodeObjectForKey:@"HFBackgroundColor"] ?: [NSColor colorWithCalibratedWhite:(CGFloat).87 alpha:1] retain]; - + _borderColor = [[NSColor controlShadowColor] retain]; + _backgroundColor = [[NSColor windowBackgroundColor] retain]; + return self; } diff --git a/HexFiend/HFLineCountingView.m b/HexFiend/HFLineCountingView.m index d111bcd..080599b 100644 --- a/HexFiend/HFLineCountingView.m +++ b/HexFiend/HFLineCountingView.m @@ -119,20 +119,6 @@ [super viewWillMoveToWindow:newWindow]; } -- (void)drawGradientWithClip:(NSRect)clip { - [_representer.backgroundColor set]; - NSRectFill(clip); - - NSInteger shadowEdge = _representer.interiorShadowEdge; - - if (shadowEdge >= 0) { - const CGFloat shadowWidth = 6; - NSWindow *window = self.window; - BOOL drawActive = (window == nil || [window isKeyWindow] || [window isMainWindow]); - HFDrawShadow([[NSGraphicsContext currentContext] graphicsPort], self.bounds, shadowWidth, shadowEdge, drawActive, clip); - } -} - - (void)drawDividerWithClip:(NSRect)clipRect { USE(clipRect); @@ -267,7 +253,7 @@ static inline int common_prefix_length(const char *a, const char *b) { NSUInteger glyphCount; [textStorage replaceCharactersInRange:replacementRange withString:replacementCharacters]; if (previousTextStorageCharacterCount == 0) { - NSDictionary *atts = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, nil]; + NSDictionary *atts = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, nil]; [textStorage setAttributes:atts range:NSMakeRange(0, newStringLength)]; [atts release]; } @@ -305,7 +291,7 @@ static inline int common_prefix_length(const char *a, const char *b) { [mutableStyle setAlignment:NSRightTextAlignment]; NSParagraphStyle *paragraphStyle = [mutableStyle copy]; [mutableStyle release]; - textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; [paragraphStyle release]; } @@ -456,12 +442,12 @@ static inline int common_prefix_length(const char *a, const char *b) { } } - if (! textAttributes) { + if (!textAttributes) { NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; [mutableStyle setAlignment:NSRightTextAlignment]; NSParagraphStyle *paragraphStyle = [mutableStyle copy]; [mutableStyle release]; - textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; [paragraphStyle release]; [textStorage setAttributes:textAttributes range:NSMakeRange(0, [textStorage length])]; } @@ -489,27 +475,28 @@ static inline int common_prefix_length(const char *a, const char *b) { NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); CGFloat linesToVerticallyOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); - CGFloat verticalOffset = linesToVerticallyOffset * _lineHeight + 1; + CGFloat verticalOffset = linesToVerticallyOffset * _lineHeight + 1.5; NSRect textRect = self.bounds; textRect.size.width -= 5; textRect.origin.y -= verticalOffset; - textRect.size.height += verticalOffset; - - if (! textAttributes) { - NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - [mutableStyle setAlignment:NSRightTextAlignment]; - [mutableStyle setMinimumLineHeight:_lineHeight]; - [mutableStyle setMaximumLineHeight:_lineHeight]; - NSParagraphStyle *paragraphStyle = [mutableStyle copy]; - [mutableStyle release]; - textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; - [paragraphStyle release]; - } + textRect.size.height += verticalOffset + _lineHeight; + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + [mutableStyle setMinimumLineHeight:_lineHeight]; + [mutableStyle setMaximumLineHeight:_lineHeight]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + NSColor *color = [[NSColor textColor] colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + color = [NSColor colorWithRed:color.redComponent green:color.greenComponent blue:color.blueComponent alpha:0.75]; + NSDictionary *_textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSFont fontWithName:_font.fontName size:9], NSFontAttributeName, color, NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + NSString *string = [self newLineStringForRange:HFRangeMake(lineIndex, linesRemaining)]; - [string drawInRect:textRect withAttributes:textAttributes]; + [string drawInRect:textRect withAttributes:_textAttributes]; [string release]; + [_textAttributes release]; } - (void)drawLineNumbersWithClipSingleStringCellDrawing:(NSRect)clipRect { @@ -533,7 +520,7 @@ static inline int common_prefix_length(const char *a, const char *b) { [mutableStyle setMaximumLineHeight:_lineHeight]; NSParagraphStyle *paragraphStyle = [mutableStyle copy]; [mutableStyle release]; - textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor colorWithCalibratedWhite:(CGFloat).1 alpha:(CGFloat).8], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; [paragraphStyle release]; } @@ -568,7 +555,7 @@ static inline int common_prefix_length(const char *a, const char *b) { #if TIME_LINE_NUMBERS CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); #endif - NSInteger drawingMode = (useStringDrawingPath ? 1 : 3); + NSInteger drawingMode = 3; // (useStringDrawingPath ? 1 : 3); switch (drawingMode) { // Drawing can't be done right if fonts are wider than expected, but all // of these have rather nasty behavior in that case. I've commented what @@ -606,7 +593,6 @@ static inline int common_prefix_length(const char *a, const char *b) { } - (void)drawRect:(NSRect)clipRect { - [self drawGradientWithClip:clipRect]; [self drawDividerWithClip:clipRect]; [self drawLineNumbersWithClip:clipRect]; } diff --git a/HexFiend/HFRepresenterTextView.m b/HexFiend/HFRepresenterTextView.m index 95a763d..7fcbd0c 100644 --- a/HexFiend/HFRepresenterTextView.m +++ b/HexFiend/HFRepresenterTextView.m @@ -441,7 +441,7 @@ enum LineCoverage_t { - (void)drawCaretIfNecessaryWithClip:(NSRect)clipRect { NSRect caretRect = NSIntersectionRect(caretRectToDraw, clipRect); if (! NSIsEmptyRect(caretRect)) { - [[NSColor blackColor] set]; + [[NSColor controlTextColor] set]; NSRectFill(caretRect); lastDrawnCaretRect = caretRect; } @@ -456,12 +456,18 @@ enum LineCoverage_t { /* This is the color when we are not in the key window */ - (NSColor *)inactiveTextSelectionColor { + if (@available(macOS 10.14, *)) { + return [NSColor unemphasizedSelectedTextBackgroundColor]; + } return [NSColor colorWithCalibratedWhite: (CGFloat)(212./255.) alpha:1]; } /* This is the color when we are not the first responder, but we are in the key window */ - (NSColor *)secondaryTextSelectionColor { - return [[self primaryTextSelectionColor] blendedColorWithFraction:.66 ofColor:[NSColor colorWithCalibratedWhite:.8f alpha:1]]; + if (@available(macOS 10.14, *)) { + return [NSColor unemphasizedSelectedTextBackgroundColor]; + } + return [NSColor colorWithCalibratedWhite: (CGFloat)(212./255.) alpha:1]; } - (NSColor *)textSelectionColor { @@ -826,7 +832,7 @@ enum LineCoverage_t { - (HFTextVisualStyleRun *)styleRunForByteAtIndex:(NSUInteger)byteIndex { HFTextVisualStyleRun *run = [[HFTextVisualStyleRun alloc] init]; [run setRange:NSMakeRange(0, NSUIntegerMax)]; - [run setForegroundColor:[NSColor blackColor]]; + [run setForegroundColor:[NSColor textColor]]; return [run autorelease]; } @@ -902,8 +908,8 @@ static size_t unionAndCleanLists(NSRect *rectList, id *valueList, size_t count) guideIndex++; } if (rectIndex > 0) { - [[NSColor colorWithCalibratedWhite:(CGFloat).8 alpha:1] set]; - NSRectFillListUsingOperation(lineRects, rectIndex, NSCompositePlusDarker); + [[NSColor gridColor] set]; + NSRectFillListUsingOperation(lineRects, rectIndex, NSCompositeSourceOver); } FREE_ARRAY(lineRects); } @@ -1344,7 +1350,7 @@ static size_t unionAndCleanLists(NSRect *rectList, id *valueList, size_t count) [self _drawDefaultLineBackgrounds:clip withLineHeight:[self lineHeight] maxLines:ll2l(HFRoundUpToNextMultipleSaturate(byteCount, bytesPerLine) / bytesPerLine)]; [self drawSelectionIfNecessaryWithClip:clip]; - NSColor *textColor = [NSColor blackColor]; + NSColor *textColor = [NSColor textColor]; [textColor set]; if (! antialias) { diff --git a/HexFiend/HFRepresenterTextViewCallout.m b/HexFiend/HFRepresenterTextViewCallout.m index ae46bd8..bb4b58e 100644 --- a/HexFiend/HFRepresenterTextViewCallout.m +++ b/HexFiend/HFRepresenterTextViewCallout.m @@ -432,7 +432,7 @@ static double distanceMod1(double a, double b) { // Compute the vertical offset CGFloat textYOffset = (glyphCount == 1 ? 4 : 5); // LOL - if ([_label isEqualToString:@"6"] || [_label isEqualToString:@"7"] == 7) textYOffset -= 1; + if ([_label isEqualToString:@"6"]) textYOffset -= 1; // Apply this text matrix diff --git a/HexFiend/HFStatusBarRepresenter.m b/HexFiend/HFStatusBarRepresenter.m index 702dfea..883677f 100644 --- a/HexFiend/HFStatusBarRepresenter.m +++ b/HexFiend/HFStatusBarRepresenter.m @@ -29,7 +29,7 @@ - (void)_sharedInitStatusBarView { NSMutableParagraphStyle *style = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease]; [style setAlignment:NSCenterTextAlignment]; - cellAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSColor colorWithCalibratedWhite:(CGFloat).15 alpha:1], NSForegroundColorAttributeName, [NSFont labelFontOfSize:10], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]; + cellAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSColor windowFrameTextColor], NSForegroundColorAttributeName, [NSFont labelFontOfSize:[NSFont smallSystemFontSize]], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]; cell = [[NSCell alloc] initTextCell:@""]; [cell setAlignment:NSCenterTextAlignment]; [cell setBackgroundStyle:NSBackgroundStyleRaised]; @@ -62,51 +62,24 @@ [self setNeedsDisplay:YES]; } -- (void)drawDividerWithClip:(NSRect)clipRect { - [[NSColor lightGrayColor] set]; - NSRect bounds = [self bounds]; - NSRect lineRect = bounds; - lineRect.size.height = 1; - NSRectFill(NSIntersectionRect(lineRect, clipRect)); -} - - -- (NSGradient *)getGradient:(BOOL)active { - static NSGradient *sActiveGradient; - static NSGradient *sInactiveGradient; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sActiveGradient = [[NSGradient alloc] initWithColorsAndLocations: - [NSColor colorWithCalibratedWhite:.89 alpha:1.], 0.00, - [NSColor colorWithCalibratedWhite:.77 alpha:1.], 0.9, - [NSColor colorWithCalibratedWhite:.82 alpha:1.], 1.0, - nil]; - - sInactiveGradient = [[NSGradient alloc] initWithColorsAndLocations: - [NSColor colorWithCalibratedWhite:.93 alpha:1.], 0.00, - [NSColor colorWithCalibratedWhite:.87 alpha:1.], 0.9, - [NSColor colorWithCalibratedWhite:.90 alpha:1.], 1.0, - nil]; - }); - return active ? sActiveGradient : sInactiveGradient; -} - - - (void)drawRect:(NSRect)clip { USE(clip); NSRect bounds = [self bounds]; // [[NSColor colorWithCalibratedWhite:(CGFloat).91 alpha:1] set]; // NSRectFill(clip); - NSWindow *window = [self window]; - BOOL drawActive = (window == nil || [window isMainWindow] || [window isKeyWindow]); - [[self getGradient:drawActive] drawInRect:bounds angle:90.]; - [self drawDividerWithClip:clip]; NSRect cellRect = NSMakeRect(NSMinX(bounds), HFCeil(NSMidY(bounds) - cellSize.height / 2), NSWidth(bounds), cellSize.height); [cell drawWithFrame:cellRect inView:self]; } +- (void)setFrame:(NSRect)frame +{ + [super setFrame:frame]; + [self.window setContentBorderThickness:frame.origin.y + frame.size.height forEdge:NSMinYEdge]; +} + + - (void)mouseDown:(NSEvent *)event { USE(event); HFStatusBarMode newMode = ([representer statusMode] + 1) % HFSTATUSMODECOUNT; @@ -122,6 +95,7 @@ - (void)viewDidMoveToWindow { HFRegisterViewForWindowAppearanceChanges(self, @selector(windowDidChangeKeyStatus:), !registeredForAppNotifications); registeredForAppNotifications = YES; + [self.window setContentBorderThickness:self.frame.origin.y + self.frame.size.height forEdge:NSMinYEdge]; [super viewDidMoveToWindow]; } diff --git a/HexFiend/HFTextRepresenter.m b/HexFiend/HFTextRepresenter.m index 276e0cb..b8793b4 100644 --- a/HexFiend/HFTextRepresenter.m +++ b/HexFiend/HFTextRepresenter.m @@ -20,9 +20,13 @@ - (instancetype)init { self = [super init]; - NSColor *color1 = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0]; - NSColor *color2 = [NSColor colorWithCalibratedRed:.87 green:.89 blue:1. alpha:1.]; - _rowBackgroundColors = [@[color1, color2] retain]; + if (@available(macOS 10.14, *)) { + _rowBackgroundColors = [[NSColor alternatingContentBackgroundColors] retain]; + } else { + NSColor *color1 = [NSColor windowBackgroundColor]; + NSColor *color2 = [NSColor colorWithDeviceWhite:0.96 alpha:1]; + _rowBackgroundColors = [@[color1, color2] retain]; + } return self; } @@ -237,7 +241,7 @@ FOREACH(HFRangeWrapper *, wrapper, selectedRanges) { HFRange selectedRange = [wrapper HFRange]; BOOL clippedRangeIsVisible; - NSRange clippedSelectedRange; + NSRange clippedSelectedRange = {0,}; /* Necessary because zero length ranges do not intersect anything */ if (selectedRange.length == 0) { /* Remember that {6, 0} is considered a subrange of {3, 3} */ diff --git a/JoyKit/ControllerConfiguration.inc b/JoyKit/ControllerConfiguration.inc new file mode 100644 index 0000000..ea3ba9a --- /dev/null +++ b/JoyKit/ControllerConfiguration.inc @@ -0,0 +1,477 @@ +#define BUTTON(x) @(JOYButtonUsageGeneric0 + (x)) +#define AXIS(x) @(JOYAxisUsageGeneric0 + (x)) +#define AXES2D(x) @(JOYAxes2DUsageGeneric0 + (x)) + +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), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(2), + @(kHIDUsage_GD_Rx): @(1), + @(kHIDUsage_GD_Ry): @(1), + @(kHIDUsage_GD_Rz): @(3), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageA), + BUTTON(2): @(JOYButtonUsageB), + BUTTON(3): @(JOYButtonUsageX), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(7): @(JOYButtonUsageLStick), + BUTTON(8): @(JOYButtonUsageRStick), + BUTTON(9): @(JOYButtonUsageStart), + BUTTON(10): @(JOYButtonUsageSelect), + BUTTON(11): @(JOYButtonUsageHome), + }, + + JOYAxisUsageMapping: @{ + AXIS(3): @(JOYAxisUsageL1), + AXIS(6): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + }, + + @(0x054C): @{ // Sony + /* Generally untested, but should work */ + + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(3), + @(kHIDUsage_GD_Rz): @(1), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageY), + BUTTON(2): @(JOYButtonUsageB), + BUTTON(3): @(JOYButtonUsageA), + BUTTON(4): @(JOYButtonUsageX), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(7): @(JOYButtonUsageL2), + BUTTON(8): @(JOYButtonUsageR2), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageHome), + BUTTON(14): @(JOYButtonUsageMisc), + }, + + JOYAxisUsageMapping: @{ + AXIS(4): @(JOYAxisUsageL1), + AXIS(5): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + } +}; + +hacksByName = @{ + @"WUP-028": @{ // Nintendo GameCube Controller Adapter + JOYReportIDFilters: @[@[@1], @[@2], @[@3], @[@4]], + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageA), + BUTTON(2): @(JOYButtonUsageB), + BUTTON(3): @(JOYButtonUsageX), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageStart), + BUTTON(6): @(JOYButtonUsageZ), + BUTTON(7): @(JOYButtonUsageR1), + BUTTON(8): @(JOYButtonUsageL1), + }, + + JOYAxisUsageMapping: @{ + AXIS(3): @(JOYAxisUsageL1), + AXIS(6): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(2), + @(kHIDUsage_GD_Rx): @(1), + @(kHIDUsage_GD_Ry): @(1), + @(kHIDUsage_GD_Rz): @(3), + }, + + JOYRumbleUsage: @1, + JOYRumbleUsagePage: @0xFF00, + + JOYConnectedUsage: @2, + JOYConnectedUsagePage: @0xFF00, + + JOYActivationReport: [NSData dataWithBytes:(uint8_t[]){0x13} length:1], + + JOYCustomReports: @{ + + // Rumble + @(-17): @[ + @{@"reportID": @(1), @"size":@1, @"offset":@0, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + @{@"reportID": @(2), @"size":@1, @"offset":@8, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + @{@"reportID": @(3), @"size":@1, @"offset":@16, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + @{@"reportID": @(4), @"size":@1, @"offset":@24, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + ], + + @(33): @[ + + // Player 1 + + @{@"reportID": @(1), @"size":@1, @"offset":@4, @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + @{@"reportID": @(1), @"size":@1, @"offset":@8, @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(1), @"size":@1, @"offset":@9, @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(1), @"size":@1, @"offset":@10,@"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(1), @"size":@1, @"offset":@11,@"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(1), @"size":@1, @"offset":@12, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(1), @"size":@1, @"offset":@13, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(1), @"size":@1, @"offset":@14, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(1), @"size":@1, @"offset":@15, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(1), @"size":@1, @"offset":@16, @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(1), @"size":@1, @"offset":@17, @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(1), @"size":@1, @"offset":@18, @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(1), @"size":@1, @"offset":@19, @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(1), @"size":@8, @"offset":@24, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@32, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(1), @"size":@8, @"offset":@40, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@48, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(1), @"size":@8, @"offset":@56, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@64, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + // Player 2 + + @{@"reportID": @(2), @"size":@1, @"offset":@(4 + 72), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + + @{@"reportID": @(2), @"size":@1, @"offset":@(8 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(2), @"size":@1, @"offset":@(9 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(2), @"size":@1, @"offset":@(10 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(2), @"size":@1, @"offset":@(11 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(2), @"size":@1, @"offset":@(12 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(2), @"size":@1, @"offset":@(13 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(2), @"size":@1, @"offset":@(14 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(2), @"size":@1, @"offset":@(15 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(2), @"size":@1, @"offset":@(16 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(2), @"size":@1, @"offset":@(17 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(2), @"size":@1, @"offset":@(18 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(2), @"size":@1, @"offset":@(19 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(2), @"size":@8, @"offset":@(24 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(2), @"size":@8, @"offset":@(32 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(2), @"size":@8, @"offset":@(40 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(2), @"size":@8, @"offset":@(48 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(2), @"size":@8, @"offset":@(56 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(2), @"size":@8, @"offset":@(64 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + // Player 3 + + @{@"reportID": @(3), @"size":@1, @"offset":@(4 + 144), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + @{@"reportID": @(3), @"size":@1, @"offset":@(8 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(3), @"size":@1, @"offset":@(9 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(3), @"size":@1, @"offset":@(10 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(3), @"size":@1, @"offset":@(11 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(3), @"size":@1, @"offset":@(12 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(3), @"size":@1, @"offset":@(13 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(3), @"size":@1, @"offset":@(14 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(3), @"size":@1, @"offset":@(15 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(3), @"size":@1, @"offset":@(16 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(3), @"size":@1, @"offset":@(17 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(3), @"size":@1, @"offset":@(18 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(3), @"size":@1, @"offset":@(19 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(3), @"size":@8, @"offset":@(24 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(3), @"size":@8, @"offset":@(32 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(3), @"size":@8, @"offset":@(40 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(3), @"size":@8, @"offset":@(48 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(3), @"size":@8, @"offset":@(56 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(3), @"size":@8, @"offset":@(64 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + // Player 4 + + @{@"reportID": @(4), @"size":@1, @"offset":@(4 + 216), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + @{@"reportID": @(4), @"size":@1, @"offset":@(8 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(4), @"size":@1, @"offset":@(9 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(4), @"size":@1, @"offset":@(10 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(4), @"size":@1, @"offset":@(11 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(4), @"size":@1, @"offset":@(12 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(4), @"size":@1, @"offset":@(13 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(4), @"size":@1, @"offset":@(14 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(4), @"size":@1, @"offset":@(15 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(4), @"size":@1, @"offset":@(16 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(4), @"size":@1, @"offset":@(17 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(4), @"size":@1, @"offset":@(18 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(4), @"size":@1, @"offset":@(19 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(4), @"size":@8, @"offset":@(24 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(4), @"size":@8, @"offset":@(32 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(4), @"size":@8, @"offset":@(40 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(4), @"size":@8, @"offset":@(48 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(4), @"size":@8, @"offset":@(56 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(4), @"size":@8, @"offset":@(64 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + + ]}, + }, + + @"GameCube Controller Adapter": @{ // GameCube Controller PC Adapter + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(3), + @(kHIDUsage_GD_Rz): @(1), + }, + JOYReportIDFilters: @[@[@1], @[@2], @[@3], @[@4]], + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageX), + BUTTON(2): @(JOYButtonUsageA), + BUTTON(3): @(JOYButtonUsageB), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(8): @(JOYButtonUsageZ), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(13): @(JOYButtonUsageDPadUp), + BUTTON(14): @(JOYButtonUsageDPadRight), + BUTTON(15): @(JOYButtonUsageDPadDown), + BUTTON(16): @(JOYButtonUsageDPadLeft), + }, + + JOYAxisUsageMapping: @{ + AXIS(4): @(JOYAxisUsageL1), + AXIS(5): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(3): @(JOYAxes2DUsageRightStick), + }, + + JOYRumbleUsage: @1, + JOYRumbleUsagePage: @0xFF00, + JOYRumbleMin: @0, + JOYRumbleMax: @255, + JOYSwapZRz: @YES, + }, + + @"Twin USB Joystick": @{ // DualShock PC Adapter + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(2), + @(kHIDUsage_GD_Rz): @(1), + }, + JOYReportIDFilters: @[@[@1], @[@2]], + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageX), + BUTTON(2): @(JOYButtonUsageA), + BUTTON(3): @(JOYButtonUsageB), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageL2), + BUTTON(6): @(JOYButtonUsageR2), + BUTTON(7): @(JOYButtonUsageL1), + BUTTON(8): @(JOYButtonUsageR1), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageDPadUp), + BUTTON(14): @(JOYButtonUsageDPadRight), + BUTTON(15): @(JOYButtonUsageDPadDown), + BUTTON(16): @(JOYButtonUsageDPadLeft), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(6): @(JOYAxes2DUsageRightStick), + }, + + JOYSwapZRz: @YES, + }, + + @"Pro Controller": @{ // Switch Pro Controller + JOYIsSwitch: @YES, + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(0), + @(kHIDUsage_GD_Rx): @(1), + @(kHIDUsage_GD_Ry): @(1), + @(kHIDUsage_GD_Rz): @(1), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageB), + BUTTON(2): @(JOYButtonUsageA), + BUTTON(3): @(JOYButtonUsageY), + BUTTON(4): @(JOYButtonUsageX), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(7): @(JOYButtonUsageL2), + BUTTON(8): @(JOYButtonUsageR2), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageHome), + BUTTON(14): @(JOYButtonUsageMisc), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + + 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}, + @{@"reportID": @(1), @"size":@1, @"offset":@19, @"usagePage":@(kHIDPage_Button), @"usage":@2}, + + // SR and SL not used on the Pro Controller + @{@"reportID": @(1), @"size":@1, @"offset":@22, @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(1), @"size":@1, @"offset":@23, @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(1), @"size":@1, @"offset":@24, @"usagePage":@(kHIDPage_Button), @"usage":@9}, + @{@"reportID": @(1), @"size":@1, @"offset":@25, @"usagePage":@(kHIDPage_Button), @"usage":@10}, + @{@"reportID": @(1), @"size":@1, @"offset":@26, @"usagePage":@(kHIDPage_Button), @"usage":@12}, + @{@"reportID": @(1), @"size":@1, @"offset":@27, @"usagePage":@(kHIDPage_Button), @"usage":@11}, + + @{@"reportID": @(1), @"size":@1, @"offset":@28, @"usagePage":@(kHIDPage_Button), @"usage":@13}, + @{@"reportID": @(1), @"size":@1, @"offset":@29, @"usagePage":@(kHIDPage_Button), @"usage":@14}, + + @{@"reportID": @(1), @"size":@1, @"offset":@32, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(1), @"size":@1, @"offset":@33, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + @{@"reportID": @(1), @"size":@1, @"offset":@34, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(1), @"size":@1, @"offset":@35, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + + // SR and SL not used on the Pro Controller + @{@"reportID": @(1), @"size":@1, @"offset":@38, @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(1), @"size":@1, @"offset":@39, @"usagePage":@(kHIDPage_Button), @"usage":@7}, + + /* Sticks */ + @{@"reportID": @(1), @"size":@12, @"offset":@40, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @0xFFF}, + @{@"reportID": @(1), @"size":@12, @"offset":@52, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @0xFFF, @"max": @0}, + + @{@"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}, + ], + }, + }, + + JOYIgnoredReports: @(0x30), // Ignore the real 0x30 report as it's broken + + @"PLAYSTATION(R)3 Controller": @{ // DualShock 3 + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(3), + @(kHIDUsage_GD_Rz): @(1), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageSelect), + BUTTON(2): @(JOYButtonUsageL3), + BUTTON(3): @(JOYButtonUsageR3), + BUTTON(4): @(JOYButtonUsageStart), + BUTTON(5): @(JOYButtonUsageDPadUp), + BUTTON(6): @(JOYButtonUsageDPadRight), + BUTTON(7): @(JOYButtonUsageDPadDown), + BUTTON(8): @(JOYButtonUsageDPadLeft), + BUTTON(9): @(JOYButtonUsageL2), + BUTTON(10): @(JOYButtonUsageR2), + BUTTON(11): @(JOYButtonUsageL1), + BUTTON(12): @(JOYButtonUsageR1), + BUTTON(13): @(JOYButtonUsageX), + BUTTON(14): @(JOYButtonUsageA), + BUTTON(15): @(JOYButtonUsageB), + BUTTON(16): @(JOYButtonUsageY), + BUTTON(17): @(JOYButtonUsageHome), + }, + + JOYAxisUsageMapping: @{ + AXIS(4): @(JOYAxisUsageL1), + AXIS(5): @(JOYAxisUsageR1), + AXIS(8): @(JOYAxisUsageL2), + AXIS(9): @(JOYAxisUsageR2), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(3): @(JOYAxes2DUsageRightStick), + }, + + JOYCustomReports: @{ + @(0x01): @[ + /* Pressure sensitive inputs */ + @{@"reportID": @(1), @"size":@8, @"offset":@(13 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(14 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(15 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(16 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(17 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Dial), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(18 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Wheel), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(19 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(20 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(21 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(22 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(23 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(24 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + ] + }, + + JOYIsDualShock3: @YES, + }, + +}; diff --git a/JoyKit/JOYAxes2D.h b/JoyKit/JOYAxes2D.h new file mode 100644 index 0000000..b6f6d15 --- /dev/null +++ b/JoyKit/JOYAxes2D.h @@ -0,0 +1,24 @@ +#import + +typedef enum { + JOYAxes2DUsageNone, + JOYAxes2DUsageLeftStick, + JOYAxes2DUsageRightStick, + JOYAxes2DUsageMiddleStick, + JOYAxes2DUsagePointer, + JOYAxes2DUsageNonGenericMax, + + JOYAxes2DUsageGeneric0 = 0x10000, +} JOYAxes2DUsage; + +@interface JOYAxes2D : NSObject +- (NSString *)usageString; ++ (NSString *)usageToString: (JOYAxes2DUsage) usage; +- (uint64_t)uniqueID; +- (double)distance; +- (double)angle; +- (NSPoint)value; +@property JOYAxes2DUsage usage; +@end + + diff --git a/JoyKit/JOYAxes2D.m b/JoyKit/JOYAxes2D.m new file mode 100644 index 0000000..272d34f --- /dev/null +++ b/JoyKit/JOYAxes2D.m @@ -0,0 +1,181 @@ +#import "JOYAxes2D.h" +#import "JOYElement.h" + +@implementation JOYAxes2D +{ + JOYElement *_element1, *_element2; + double _state1, _state2; + int32_t initialX, initialY; + int32_t minX, minY; + int32_t maxX, maxY; + +} + ++ (NSString *)usageToString: (JOYAxes2DUsage) usage +{ + if (usage < JOYAxes2DUsageNonGenericMax) { + return (NSString *[]) { + @"None", + @"Left Stick", + @"Right Stick", + @"Middle Stick", + @"Pointer", + }[usage]; + } + if (usage >= JOYAxes2DUsageGeneric0) { + return [NSString stringWithFormat:@"Generic 2D Analog Control %d", usage - JOYAxes2DUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage 2D 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 degrees>", self.className, self, self.usageString, self.uniqueID, self.distance * 100, self.angle]; +} + +- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2 +{ + self = [super init]; + if (!self) return self; + + _element1 = element1; + _element2 = element2; + + + if (element1.usagePage == kHIDPage_GenericDesktop) { + uint16_t usage = element1.usage; + _usage = JOYAxes2DUsageGeneric0 + usage - kHIDUsage_GD_X + 1; + } + initialX = 0; + initialY = 0; + minX = element1.max; + minY = element2.max; + maxX = element1.min; + maxY = element2.min; + + return self; +} + +- (NSPoint)value +{ + return NSMakePoint(_state1, _state2); +} + +-(int32_t) effectiveMinX +{ + int32_t rawMin = _element1.min; + int32_t rawMax = _element1.max; + if (initialX == 0) return rawMin; + if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return minX; + if ((initialX - rawMin) < (rawMax - initialX)) return rawMin; + return initialX - (rawMax - initialX); +} + +-(int32_t) effectiveMinY +{ + int32_t rawMin = _element2.min; + int32_t rawMax = _element2.max; + if (initialY == 0) return rawMin; + if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return minY; + if ((initialY - rawMin) < (rawMax - initialY)) return rawMin; + return initialY - (rawMax - initialY); +} + +-(int32_t) effectiveMaxX +{ + int32_t rawMin = _element1.min; + int32_t rawMax = _element1.max; + if (initialX == 0) return rawMax; + if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return maxX; + if ((initialX - rawMin) > (rawMax - initialX)) return rawMax; + return initialX + (initialX - rawMin); +} + +-(int32_t) effectiveMaxY +{ + int32_t rawMin = _element2.min; + int32_t rawMax = _element2.max; + if (initialY == 0) return rawMax; + if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return maxY; + if ((initialY - rawMin) > (rawMax - initialY)) return rawMax; + return initialY + (initialY - rawMin); +} + +- (bool)updateState +{ + int32_t x = [_element1 value]; + int32_t y = [_element2 value]; + if (x == 0 && y == 0) return false; + + if (initialX == 0 && initialY == 0) { + initialX = x; + initialY = y; + } + + double old1 = _state1, old2 = _state2; + { + int32_t value = x; + + if (initialX != 0) { + minX = MIN(value, minX); + maxX = MAX(value, maxX); + } + + double min = [self effectiveMinX]; + double max = [self effectiveMaxX]; + if (min == max) return false; + + _state1 = (value - min) / (max - min) * 2 - 1; + } + + { + int32_t value = y; + + if (initialY != 0) { + minY = MIN(value, minY); + maxY = MAX(value, maxY); + } + + double min = [self effectiveMinY]; + double max = [self effectiveMaxY]; + if (min == max) return false; + + _state2 = (value - min) / (max - min) * 2 - 1; + } + + if (_state1 < -1 || _state1 > 1 || + _state2 < -1 || _state2 > 1) { + // Makes no sense, recalibrate + _state1 = _state2 = 0; + initialX = initialY = 0; + minX = _element1.max; + minY = _element2.max; + maxX = _element1.min; + maxY = _element2.min; + } + + return old1 != _state1 || old2 != _state2; +} + +- (double)distance +{ + return MIN(sqrt(_state1 * _state1 + _state2 * _state2), 1.0); +} + +- (double)angle { + double temp = atan2(_state2, _state1) * 180 / M_PI; + if (temp >= 0) return temp; + return temp + 360; +} +@end diff --git a/JoyKit/JOYAxis.h b/JoyKit/JOYAxis.h new file mode 100644 index 0000000..5a4c166 --- /dev/null +++ b/JoyKit/JOYAxis.h @@ -0,0 +1,29 @@ +#import + +typedef enum { + JOYAxisUsageNone, + JOYAxisUsageL1, + JOYAxisUsageL2, + JOYAxisUsageL3, + JOYAxisUsageR1, + JOYAxisUsageR2, + JOYAxisUsageR3, + JOYAxisUsageWheel, + JOYAxisUsageRudder, + JOYAxisUsageThrottle, + JOYAxisUsageAccelerator, + JOYAxisUsageBrake, + JOYAxisUsageNonGenericMax, + + JOYAxisUsageGeneric0 = 0x10000, +} JOYAxisUsage; + +@interface JOYAxis : NSObject +- (NSString *)usageString; ++ (NSString *)usageToString: (JOYAxisUsage) usage; +- (uint64_t)uniqueID; +- (double)value; +@property JOYAxisUsage usage; +@end + + diff --git a/JoyKit/JOYAxis.m b/JoyKit/JOYAxis.m new file mode 100644 index 0000000..169eaee --- /dev/null +++ b/JoyKit/JOYAxis.m @@ -0,0 +1,90 @@ +#import "JOYAxis.h" +#import "JOYElement.h" + +@implementation JOYAxis +{ + JOYElement *_element; + double _state; + double _min; +} + ++ (NSString *)usageToString: (JOYAxisUsage) usage +{ + if (usage < JOYAxisUsageNonGenericMax) { + return (NSString *[]) { + @"None", + @"Analog L1", + @"Analog L2", + @"Analog L3", + @"Analog R1", + @"Analog R2", + @"Analog R3", + @"Wheel", + @"Rudder", + @"Throttle", + @"Accelerator", + @"Brake", + }[usage]; + } + if (usage >= JOYAxisUsageGeneric0) { + return [NSString stringWithFormat:@"Generic Analog Control %d", usage - JOYAxisUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage Axis %d", usage]; +} + +- (NSString *)usageString +{ + return [self.class usageToString:_usage]; +} + +- (uint64_t)uniqueID +{ + return _element.uniqueID; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %f%%>", self.className, self, self.usageString, self.uniqueID, _state * 100]; +} + +- (instancetype)initWithElement:(JOYElement *)element +{ + self = [super init]; + if (!self) return self; + + _element = element; + + + if (element.usagePage == kHIDPage_GenericDesktop) { + uint16_t usage = element.usage; + _usage = JOYAxisUsageGeneric0 + usage - kHIDUsage_GD_X + 1; + } + + _min = 1.0; + + return self; +} + +- (double) value +{ + return _state; +} + +- (bool)updateState +{ + double min = _element.min; + double max = _element.max; + if (min == max) return false; + double old = _state; + double unnormalized = ([_element value] - min) / (max - min); + if (unnormalized < _min) { + _min = unnormalized; + } + if (_min != 1) { + _state = (unnormalized - _min) / (1 - _min); + } + return old != _state; +} + +@end diff --git a/JoyKit/JOYButton.h b/JoyKit/JOYButton.h new file mode 100644 index 0000000..f732c8e --- /dev/null +++ b/JoyKit/JOYButton.h @@ -0,0 +1,42 @@ +#import + + + +typedef enum { + JOYButtonUsageNone, + JOYButtonUsageA, + JOYButtonUsageB, + JOYButtonUsageC, + JOYButtonUsageX, + JOYButtonUsageY, + JOYButtonUsageZ, + JOYButtonUsageStart, + JOYButtonUsageSelect, + JOYButtonUsageHome, + JOYButtonUsageMisc, + JOYButtonUsageLStick, + JOYButtonUsageRStick, + JOYButtonUsageL1, + JOYButtonUsageL2, + JOYButtonUsageL3, + JOYButtonUsageR1, + JOYButtonUsageR2, + JOYButtonUsageR3, + JOYButtonUsageDPadLeft, + JOYButtonUsageDPadRight, + JOYButtonUsageDPadUp, + JOYButtonUsageDPadDown, + JOYButtonUsageNonGenericMax, + + JOYButtonUsageGeneric0 = 0x10000, +} JOYButtonUsage; + +@interface JOYButton : NSObject +- (NSString *)usageString; ++ (NSString *)usageToString: (JOYButtonUsage) usage; +- (uint64_t)uniqueID; +- (bool) isPressed; +@property JOYButtonUsage usage; +@end + + diff --git a/JoyKit/JOYButton.m b/JoyKit/JOYButton.m new file mode 100644 index 0000000..3e6026d --- /dev/null +++ b/JoyKit/JOYButton.m @@ -0,0 +1,102 @@ +#import "JOYButton.h" +#import "JOYElement.h" + +@implementation JOYButton +{ + JOYElement *_element; + bool _state; +} + ++ (NSString *)usageToString: (JOYButtonUsage) usage +{ + if (usage < JOYButtonUsageNonGenericMax) { + return (NSString *[]) { + @"None", + @"A", + @"B", + @"C", + @"X", + @"Y", + @"Z", + @"Start", + @"Select", + @"Home", + @"Misc", + @"Left Stick", + @"Right Stick", + @"L1", + @"L2", + @"L3", + @"R1", + @"R2", + @"R3", + @"D-Pad Left", + @"D-Pad Right", + @"D-Pad Up", + @"D-Pad Down", + }[usage]; + } + if (usage >= JOYButtonUsageGeneric0) { + return [NSString stringWithFormat:@"Generic Button %d", usage - JOYButtonUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage Button %d", usage]; +} + +- (NSString *)usageString +{ + return [self.class usageToString:_usage]; +} + +- (uint64_t)uniqueID +{ + return _element.uniqueID; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %s>", self.className, self, self.usageString, self.uniqueID, _state? "Presssed" : "Released"]; +} + +- (instancetype)initWithElement:(JOYElement *)element +{ + self = [super init]; + if (!self) return self; + + _element = element; + + if (element.usagePage == kHIDPage_Button) { + uint16_t usage = element.usage; + _usage = JOYButtonUsageGeneric0 + usage; + } + else if (element.usagePage == kHIDPage_GenericDesktop) { + switch (element.usage) { + case kHIDUsage_GD_DPadUp: _usage = JOYButtonUsageDPadUp; break; + case kHIDUsage_GD_DPadDown: _usage = JOYButtonUsageDPadDown; break; + case kHIDUsage_GD_DPadRight: _usage = JOYButtonUsageDPadRight; break; + case kHIDUsage_GD_DPadLeft: _usage = JOYButtonUsageDPadLeft; break; + case kHIDUsage_GD_Start: _usage = JOYButtonUsageStart; break; + case kHIDUsage_GD_Select: _usage = JOYButtonUsageSelect; break; + case kHIDUsage_GD_SystemMainMenu: _usage = JOYButtonUsageHome; break; + } + } + + return self; +} + +- (bool) isPressed +{ + return _state; +} + +- (bool)updateState +{ + bool state = [_element value]; + if (_state != state) { + _state = state; + return true; + } + return false; +} + +@end diff --git a/JoyKit/JOYController.h b/JoyKit/JOYController.h new file mode 100644 index 0000000..9ed7cf7 --- /dev/null +++ b/JoyKit/JOYController.h @@ -0,0 +1,41 @@ +#import +#import "JOYButton.h" +#import "JOYAxis.h" +#import "JOYAxes2D.h" +#import "JOYHat.h" + +static NSString const *JOYAxesEmulateButtonsKey = @"JOYAxesEmulateButtons"; +static NSString const *JOYAxes2DEmulateButtonsKey = @"JOYAxes2DEmulateButtons"; +static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; + +@class JOYController; + +@protocol JOYListener + +@optional +-(void) controllerConnected:(JOYController *)controller; +-(void) controllerDisconnected:(JOYController *)controller; +-(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 movedHat:(JOYHat *)hat; + +@end + +@interface JOYController : NSObject ++ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options; ++ (NSArray *) allControllers; ++ (void) registerListener:(id)listener; ++ (void) unregisterListener:(id)listener; +- (NSString *)deviceName; +- (NSString *)uniqueID; +- (NSArray *) buttons; +- (NSArray *) axes; +- (NSArray *) axes2D; +- (NSArray *) hats; +- (void)setRumbleAmplitude:(double)amp; +- (void)setPlayerLEDs:(uint8_t)mask; +@property (readonly, getter=isConnected) bool connected; +@end + + diff --git a/JoyKit/JOYController.m b/JoyKit/JOYController.m new file mode 100644 index 0000000..ca2d1b1 --- /dev/null +++ b/JoyKit/JOYController.m @@ -0,0 +1,913 @@ +#import "JOYController.h" +#import "JOYMultiplayerController.h" +#import "JOYElement.h" +#import "JOYSubElement.h" +#import "JOYFullReportElement.h" + +#import "JOYEmulatedButton.h" +#include + +#define PWM_RESOLUTION 16 + +static NSString const *JOYAxisGroups = @"JOYAxisGroups"; +static NSString const *JOYReportIDFilters = @"JOYReportIDFilters"; +static NSString const *JOYButtonUsageMapping = @"JOYButtonUsageMapping"; +static NSString const *JOYAxisUsageMapping = @"JOYAxisUsageMapping"; +static NSString const *JOYAxes2DUsageMapping = @"JOYAxes2DUsageMapping"; +static NSString const *JOYCustomReports = @"JOYCustomReports"; +static NSString const *JOYIsSwitch = @"JOYIsSwitch"; +static NSString const *JOYRumbleUsage = @"JOYRumbleUsage"; +static NSString const *JOYRumbleUsagePage = @"JOYRumbleUsagePage"; +static NSString const *JOYConnectedUsage = @"JOYConnectedUsage"; +static NSString const *JOYConnectedUsagePage = @"JOYConnectedUsagePage"; +static NSString const *JOYRumbleMin = @"JOYRumbleMin"; +static NSString const *JOYRumbleMax = @"JOYRumbleMax"; +static NSString const *JOYSwapZRz = @"JOYSwapZRz"; +static NSString const *JOYActivationReport = @"JOYActivationReport"; +static NSString const *JOYIgnoredReports = @"JOYIgnoredReports"; +static NSString const *JOYIsDualShock3 = @"JOYIsDualShock3"; + +static NSMutableDictionary *controllers; // Physical controllers +static NSMutableArray *exposedControllers; // Logical controllers + +static NSDictionary *hacksByName = nil; +static NSDictionary *hacksByManufacturer = nil; + +static NSMutableSet> *listeners = nil; + +static bool axesEmulateButtons = false; +static bool axes2DEmulateButtons = false; +static bool hatsEmulateButtons = false; + +@interface JOYController () ++ (void)controllerAdded:(IOHIDDeviceRef) device; ++ (void)controllerRemoved:(IOHIDDeviceRef) device; +- (void)elementChanged:(IOHIDElementRef) element; +- (void)gotReport:(NSData *)report; + +@end + +@interface JOYButton () +- (instancetype)initWithElement:(JOYElement *)element; +- (bool)updateState; +@end + +@interface JOYAxis () +- (instancetype)initWithElement:(JOYElement *)element; +- (bool)updateState; +@end + +@interface JOYHat () +- (instancetype)initWithElement:(JOYElement *)element; +- (bool)updateState; +@end + +@interface JOYAxes2D () +- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2; +- (bool)updateState; +@end + +static NSDictionary *CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage) +{ + return @{ + @kIOHIDDeviceUsagePageKey: @(page), + @kIOHIDDeviceUsageKey: @(usage), + }; +} + +static void HIDDeviceAdded(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + [JOYController controllerAdded:device]; +} + +static void HIDDeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + [JOYController controllerRemoved:device]; +} + +static void HIDInput(void *context, IOReturn result, void *sender, IOHIDValueRef value) +{ + [(__bridge JOYController *)context elementChanged:IOHIDValueGetElement(value)]; +} + +static void HIDReport(void *context, IOReturn result, void *sender, IOHIDReportType type, + uint32_t reportID, uint8_t *report, CFIndex reportLength) +{ + if (reportLength) { + [(__bridge JOYController *)context gotReport:[[NSData alloc] initWithBytesNoCopy:report length:reportLength freeWhenDone:NO]]; + } +} + +typedef struct __attribute__((packed)) { + uint8_t reportID; + uint8_t sequence; + uint8_t rumbleData[8]; + uint8_t command; + uint8_t commandData[26]; +} JOYSwitchPacket; + +typedef struct __attribute__((packed)) { + uint8_t reportID; + uint8_t padding; + uint8_t rumbleRightDuration; + uint8_t rumbleRightStrength; + uint8_t rumbleLeftDuration; + uint8_t rumbleLeftStrength; + uint32_t padding2; + uint8_t ledsEnabled; + struct { + uint8_t timeEnabled; + uint8_t dutyLength; + uint8_t enabled; + uint8_t dutyOff; + uint8_t dutyOn; + } __attribute__((packed)) led[5]; + uint8_t padding3[13]; +} JOYDualShock3Output; + +typedef union { + JOYSwitchPacket switchPacket; + JOYDualShock3Output ds3Output; +} JOYVendorSpecificOutput; + +@implementation JOYController +{ + IOHIDDeviceRef _device; + NSMutableDictionary *_buttons; + NSMutableDictionary *_axes; + NSMutableDictionary *_axes2D; + NSMutableDictionary *_hats; + NSMutableDictionary *_fullReportElements; + NSMutableDictionary *> *_multiElements; + + // Button emulation + NSMutableDictionary *_axisEmulatedButtons; + NSMutableDictionary *> *_axes2DEmulatedButtons; + NSMutableDictionary *> *_hatEmulatedButtons; + + JOYElement *_rumbleElement; + JOYElement *_connectedElement; + NSMutableDictionary *_iokitToJOY; + NSString *_serialSuffix; + bool _isSwitch; // Does this controller use the Switch protocol? + bool _isDualShock3; // Does this controller use DS3 outputs? + JOYVendorSpecificOutput _lastVendorSpecificOutput; + volatile double _rumbleAmplitude; + bool _physicallyConnected; + bool _logicallyConnected; + + NSDictionary *_hacks; + NSMutableData *_lastReport; + + // Used when creating inputs + JOYElement *_previousAxisElement; + + uint8_t _playerLEDs; + double _sentRumbleAmp; + unsigned _rumbleCounter; + bool _deviceCantSendReports; +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks +{ + return [self initWithDevice:device reportIDFilter:nil serialSuffix:nil hacks:hacks]; +} + +-(void)createOutputForElement:(JOYElement *)element +{ + uint16_t rumbleUsagePage = (uint16_t)[_hacks[JOYRumbleUsagePage] unsignedIntValue]; + uint16_t rumbleUsage = (uint16_t)[_hacks[JOYRumbleUsage] unsignedIntValue]; + + if (!_rumbleElement && rumbleUsage && rumbleUsagePage && element.usage == rumbleUsage && element.usagePage == rumbleUsagePage) { + if (_hacks[JOYRumbleMin]) { + element.min = [_hacks[JOYRumbleMin] unsignedIntValue]; + } + if (_hacks[JOYRumbleMax]) { + element.max = [_hacks[JOYRumbleMax] unsignedIntValue]; + } + _rumbleElement = element; + } +} + +-(void)createInputForElement:(JOYElement *)element +{ + uint16_t connectedUsagePage = (uint16_t)[_hacks[JOYConnectedUsagePage] unsignedIntValue]; + uint16_t connectedUsage = (uint16_t)[_hacks[JOYConnectedUsage] unsignedIntValue]; + + if (!_connectedElement && connectedUsage && connectedUsagePage && element.usage == connectedUsage && element.usagePage == connectedUsagePage) { + _connectedElement = element; + _logicallyConnected = element.value != element.min; + return; + } + + if (element.usagePage == kHIDPage_Button) { + button: { + JOYButton *button = [[JOYButton alloc] initWithElement: element]; + [_buttons setObject:button forKey:element]; + NSNumber *replacementUsage = _hacks[JOYButtonUsageMapping][@(button.usage)]; + if (replacementUsage) { + button.usage = [replacementUsage unsignedIntValue]; + } + return; + } + } + 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: + case kHIDUsage_GD_Z: + case kHIDUsage_GD_Rx: + case kHIDUsage_GD_Ry: + case kHIDUsage_GD_Rz: { + + JOYElement *other = _previousAxisElement; + _previousAxisElement = element; + if (!other) goto single; + if (other.usage >= element.usage) goto single; + if (other.reportID != element.reportID) goto single; + if (![axisGroups[@(other.usage)] isEqualTo: axisGroups[@(element.usage)]]) goto single; + if (other.parentID != element.parentID) goto single; + + JOYAxes2D *axes = nil; + if (other.usage == kHIDUsage_GD_Z && element.usage == kHIDUsage_GD_Rz && [_hacks[JOYSwapZRz] boolValue]) { + axes = [[JOYAxes2D alloc] initWithFirstElement:element secondElement:other]; + } + else { + axes = [[JOYAxes2D alloc] initWithFirstElement:other secondElement:element]; + } + NSNumber *replacementUsage = _hacks[JOYAxes2DUsageMapping][@(axes.usage)]; + if (replacementUsage) { + axes.usage = [replacementUsage unsignedIntValue]; + } + + [_axisEmulatedButtons removeObjectForKey:@(_axes[other].uniqueID)]; + [_axes removeObjectForKey:other]; + _previousAxisElement = nil; + _axes2D[other] = axes; + _axes2D[element] = axes; + + 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], + ]; + } + + /* + 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: { + JOYAxis *axis = [[JOYAxis alloc] initWithElement: element]; + [_axes setObject:axis forKey:element]; + + NSNumber *replacementUsage = _hacks[JOYAxisUsageMapping][@(axis.usage)]; + if (replacementUsage) { + axis.usage = [replacementUsage unsignedIntValue]; + } + + if (axesEmulateButtons && axis.usage >= JOYAxisUsageL1 && axis.usage <= JOYAxisUsageR3) { + _axisEmulatedButtons[@(axis.uniqueID)] = + [[JOYEmulatedButton alloc] initWithUsage:axis.usage - JOYAxisUsageL1 + JOYButtonUsageL1 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: + case kHIDUsage_GD_DPadLeft: + case kHIDUsage_GD_Start: + case kHIDUsage_GD_Select: + case kHIDUsage_GD_SystemMainMenu: + goto button; + + case kHIDUsage_GD_Hatswitch: { + JOYHat *hat = [[JOYHat alloc] initWithElement: element]; + [_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], + ]; + } + break; + } + } + } +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef)device reportIDFilter:(NSArray *) filter serialSuffix:(NSString *)suffix hacks:(NSDictionary *)hacks +{ + self = [super init]; + if (!self) return self; + + _physicallyConnected = true; + _logicallyConnected = true; + _device = (IOHIDDeviceRef)CFRetain(device); + _serialSuffix = suffix; + _playerLEDs = -1; + + IOHIDDeviceRegisterInputValueCallback(device, HIDInput, (void *)self); + IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + + NSArray *array = CFBridgingRelease(IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone)); + _buttons = [NSMutableDictionary dictionary]; + _axes = [NSMutableDictionary dictionary]; + _axes2D = [NSMutableDictionary dictionary]; + _hats = [NSMutableDictionary dictionary]; + _axisEmulatedButtons = [NSMutableDictionary dictionary]; + _axes2DEmulatedButtons = [NSMutableDictionary dictionary]; + _hatEmulatedButtons = [NSMutableDictionary dictionary]; + _iokitToJOY = [NSMutableDictionary dictionary]; + + + //NSMutableArray *axes3d = [NSMutableArray array]; + + _hacks = hacks; + _isSwitch = [_hacks[JOYIsSwitch] boolValue]; + _isDualShock3 = [_hacks[JOYIsDualShock3] boolValue]; + + NSDictionary *customReports = hacks[JOYCustomReports]; + _lastReport = [NSMutableData dataWithLength:MAX( + MAX( + [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxInputReportSizeKey)) unsignedIntValue], + [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxOutputReportSizeKey)) unsignedIntValue] + ), + [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxFeatureReportSizeKey)) unsignedIntValue] + )]; + IOHIDDeviceRegisterInputReportCallback(device, _lastReport.mutableBytes, _lastReport.length, HIDReport, (void *)self); + + if (hacks[JOYCustomReports]) { + _multiElements = [NSMutableDictionary dictionary]; + _fullReportElements = [NSMutableDictionary dictionary]; + + + for (NSNumber *_reportID in customReports) { + signed reportID = [_reportID intValue]; + bool isOutput = false; + if (reportID < 0) { + isOutput = true; + reportID = -reportID; + } + + JOYFullReportElement *element = [[JOYFullReportElement alloc] initWithDevice:device reportID:reportID]; + NSMutableArray *elements = [NSMutableArray array]; + for (NSDictionary *subElementDef in customReports[_reportID]) { + if (filter && subElementDef[@"reportID"] && ![filter containsObject:subElementDef[@"reportID"]]) continue; + JOYSubElement *subElement = [[JOYSubElement alloc] initWithRealElement:element + size:subElementDef[@"size"].unsignedLongValue + offset:subElementDef[@"offset"].unsignedLongValue + 8 // Compensate for the reportID + usagePage:subElementDef[@"usagePage"].unsignedLongValue + usage:subElementDef[@"usage"].unsignedLongValue + min:subElementDef[@"min"].unsignedIntValue + max:subElementDef[@"max"].unsignedIntValue]; + [elements addObject:subElement]; + if (isOutput) { + [self createOutputForElement:subElement]; + } + else { + [self createInputForElement:subElement]; + } + } + _multiElements[element] = elements; + if (!isOutput) { + _fullReportElements[@(reportID)] = element; + } + } + } + + id previous = nil; + NSSet *ignoredReports = nil; + if (hacks[ignoredReports]) { + ignoredReports = [NSSet setWithArray:hacks[ignoredReports]]; + } + + for (id _element in array) { + if (_element == previous) continue; // Some elements are reported twice for some reason + previous = _element; + JOYElement *element = [[JOYElement alloc] initWithElement:(__bridge IOHIDElementRef)_element]; + + bool isOutput = false; + if (filter && ![filter containsObject:@(element.reportID)]) continue; + + switch (IOHIDElementGetType((__bridge IOHIDElementRef)_element)) { + /* Handled */ + case kIOHIDElementTypeInput_Misc: + case kIOHIDElementTypeInput_Button: + case kIOHIDElementTypeInput_Axis: + break; + case kIOHIDElementTypeOutput: + isOutput = true; + break; + /* Ignored */ + default: + case kIOHIDElementTypeInput_ScanCodes: + case kIOHIDElementTypeInput_NULL: + case kIOHIDElementTypeFeature: + case kIOHIDElementTypeCollection: + continue; + } + if ((!isOutput && [ignoredReports containsObject:@(element.reportID)]) || + (isOutput && [ignoredReports containsObject:@(-element.reportID)])) continue; + + + if (IOHIDElementIsArray((__bridge IOHIDElementRef)_element)) continue; + + if (isOutput) { + [self createOutputForElement:element]; + } + else { + [self createInputForElement:element]; + } + + _iokitToJOY[@(IOHIDElementGetCookie((__bridge IOHIDElementRef)_element))] = element; + } + + [exposedControllers addObject:self]; + if (_logicallyConnected) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerConnected:)]) { + [listener controllerConnected:self]; + } + } + } + + if (_hacks[JOYActivationReport]) { + [self sendReport:hacks[JOYActivationReport]]; + } + + if (_isSwitch) { + [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x04} length:2]]; + [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x02} length:2]]; + } + + if (_isDualShock3) { + _lastVendorSpecificOutput.ds3Output = (JOYDualShock3Output){ + .reportID = 1, + .led = { + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0, .dutyLength = 0, .enabled = 0, .dutyOff = 0, .dutyOn = 0}, + } + }; + + } + + return self; +} + +- (NSString *)deviceName +{ + if (!_device) return nil; + return IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey)); +} + +- (NSString *)uniqueID +{ + if (!_device) return nil; + NSString *serial = (__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDSerialNumberKey)); + if (!serial || [(__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDTransportKey)) isEqualToString:@"USB"]) { + serial = [NSString stringWithFormat:@"%04x%04x%08x", + [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDVendorIDKey)) unsignedIntValue], + [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductIDKey)) unsignedIntValue], + [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDLocationIDKey)) unsignedIntValue]]; + } + if (_serialSuffix) { + return [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix]; + } + return serial; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@, %@>", self.className, self, self.deviceName, self.uniqueID]; +} + +- (NSArray *)buttons +{ + NSMutableArray *ret = [[_buttons allValues] mutableCopy]; + [ret addObjectsFromArray:_axisEmulatedButtons.allValues]; + for (NSArray *array in _axes2DEmulatedButtons.allValues) { + [ret addObjectsFromArray:array]; + } + for (NSArray *array in _hatEmulatedButtons.allValues) { + [ret addObjectsFromArray:array]; + } + return ret; +} + +- (NSArray *)axes +{ + return [_axes allValues]; +} + +- (NSArray *)axes2D +{ + return [[NSSet setWithArray:[_axes2D allValues]] allObjects]; +} + +- (NSArray *)hats +{ + return [_hats allValues]; +} + +- (void)gotReport:(NSData *)report +{ + JOYFullReportElement *element = _fullReportElements[@(*(uint8_t *)report.bytes)]; + if (element) { + [element updateValue:report]; + + NSArray *subElements = _multiElements[element]; + if (subElements) { + for (JOYElement *subElement in subElements) { + [self _elementChanged:subElement]; + } + } + } + [self updateRumble]; +} + +- (void)elementChanged:(IOHIDElementRef)element +{ + JOYElement *_element = _iokitToJOY[@(IOHIDElementGetCookie(element))]; + if (_element) { + [self _elementChanged:_element]; + } + else { + //NSLog(@"Unhandled usage %x (Cookie: %x, Usage: %x)", IOHIDElementGetUsage(element), IOHIDElementGetCookie(element), IOHIDElementGetUsage(element)); + } +} + +- (void)_elementChanged:(JOYElement *)element +{ + if (element == _connectedElement) { + bool old = self.connected; + _logicallyConnected = _connectedElement.value != _connectedElement.min; + if (!old && self.connected) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerConnected:)]) { + [listener controllerConnected:self]; + } + } + } + else if (old && !self.connected) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { + [listener controllerDisconnected:self]; + } + } + } + } + + if (!self.connected) return; + { + JOYButton *button = _buttons[element]; + if (button) { + if ([button updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + return; + } + } + + + { + JOYAxis *axis = _axes[element]; + if (axis) { + if ([axis updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedAxis:)]) { + [listener controller:self movedAxis:axis]; + } + } + JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID)]; + if ([button updateStateFromAxis:axis]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + } + return; + } + } + + { + JOYAxes2D *axes = _axes2D[element]; + if (axes) { + if ([axes updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedAxes2D:)]) { + [listener controller:self movedAxes2D:axes]; + } + } + NSArray *buttons = _axes2DEmulatedButtons[@(axes.uniqueID)]; + for (JOYEmulatedButton *button in buttons) { + if ([button updateStateFromAxes2D:axes]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + } + } + return; + } + } + + { + JOYHat *hat = _hats[element]; + if (hat) { + if ([hat updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedHat:)]) { + [listener controller:self movedHat:hat]; + } + } + + NSArray *buttons = _hatEmulatedButtons[@(hat.uniqueID)]; + for (JOYEmulatedButton *button in buttons) { + if ([button updateStateFromHat:hat]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + } + } + return; + } + } +} + +- (void)disconnected +{ + if (_logicallyConnected && [exposedControllers containsObject:self]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { + [listener controllerDisconnected:self]; + } + } + } + _physicallyConnected = false; + [exposedControllers removeObject:self]; + [self setRumbleAmplitude:0]; + [self updateRumble]; + _device = nil; +} + +- (void)sendReport:(NSData *)report +{ + if (!report.length) return; + if (!_device) return; + if (_deviceCantSendReports) return; + /* Some Macs fail to send reports to some devices, specifically the DS3, returning the bogus(?) error code 1 after + freezing for 5 seconds. Stop sending reports if that's the case. */ + if (IOHIDDeviceSetReport(_device, kIOHIDReportTypeOutput, *(uint8_t *)report.bytes, report.bytes, report.length) == 1) { + _deviceCantSendReports = true; + NSLog(@"This Mac appears to be incapable of sending output reports to %@", self); + } +} + +- (void)setPlayerLEDs:(uint8_t)mask +{ + mask &= 0xF; + if (mask == _playerLEDs) { + return; + } + _playerLEDs = mask; + if (_isSwitch) { + _lastVendorSpecificOutput.switchPacket.reportID = 0x1; // Rumble and LEDs + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 0x30; // LED + _lastVendorSpecificOutput.switchPacket.commandData[0] = mask; + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + } + else if (_isDualShock3) { + _lastVendorSpecificOutput.ds3Output.reportID = 1; + _lastVendorSpecificOutput.ds3Output.ledsEnabled = mask << 1; + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]]; + } +} + +- (void)updateRumble +{ + if (!self.connected) { + return; + } + if (!_rumbleElement && !_isSwitch && !_isDualShock3) { + return; + } + if (_rumbleElement.max == 1 && _rumbleElement.min == 0) { + double ampToSend = _rumbleCounter < round(_rumbleAmplitude * PWM_RESOLUTION); + if (ampToSend != _sentRumbleAmp) { + [_rumbleElement setValue:ampToSend]; + _sentRumbleAmp = ampToSend; + } + _rumbleCounter += round(_rumbleAmplitude * PWM_RESOLUTION); + if (_rumbleCounter >= PWM_RESOLUTION) { + _rumbleCounter -= PWM_RESOLUTION; + } + } + else { + if (_rumbleAmplitude == _sentRumbleAmp) { + return; + } + _sentRumbleAmp = _rumbleAmplitude; + if (_isSwitch) { + double frequency = 144; + double amp = _rumbleAmplitude; + + uint8_t highAmp = amp * 0x64; + uint8_t lowAmp = amp * 0x32 + 0x40; + if (frequency < 0) frequency = 0; + if (frequency > 1252) frequency = 1252; + uint8_t encodedFrequency = (uint8_t)round(log2(frequency / 10.0) * 32.0); + + uint16_t highFreq = (encodedFrequency - 0x60) * 4; + uint8_t lowFreq = encodedFrequency - 0x40; + + //if (frequency < 82 || frequency > 312) { + if (amp) { + highAmp = 0; + } + + if (frequency < 40 || frequency > 626) { + lowAmp = 0; + } + + _lastVendorSpecificOutput.switchPacket.rumbleData[0] = _lastVendorSpecificOutput.switchPacket.rumbleData[4] = highFreq & 0xFF; + _lastVendorSpecificOutput.switchPacket.rumbleData[1] = _lastVendorSpecificOutput.switchPacket.rumbleData[5] = (highAmp << 1) + ((highFreq >> 8) & 0x1); + _lastVendorSpecificOutput.switchPacket.rumbleData[2] = _lastVendorSpecificOutput.switchPacket.rumbleData[6] = lowFreq; + _lastVendorSpecificOutput.switchPacket.rumbleData[3] = _lastVendorSpecificOutput.switchPacket.rumbleData[7] = lowAmp; + + + _lastVendorSpecificOutput.switchPacket.reportID = 0x10; // Rumble only + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 0; // LED + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + } + else if (_isDualShock3) { + _lastVendorSpecificOutput.ds3Output.reportID = 1; + _lastVendorSpecificOutput.ds3Output.rumbleLeftDuration = _lastVendorSpecificOutput.ds3Output.rumbleRightDuration = _rumbleAmplitude? 0xff : 0; + _lastVendorSpecificOutput.ds3Output.rumbleLeftStrength = _lastVendorSpecificOutput.ds3Output.rumbleRightStrength = round(_rumbleAmplitude * 0xff); + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]]; + } + else { + [_rumbleElement setValue:_rumbleAmplitude * (_rumbleElement.max - _rumbleElement.min) + _rumbleElement.min]; + } + } +} + +- (void)setRumbleAmplitude:(double)amp /* andFrequency: (double)frequency */ +{ + if (amp < 0) amp = 0; + if (amp > 1) amp = 1; + _rumbleAmplitude = amp; +} + +- (bool)isConnected +{ + return _logicallyConnected && _physicallyConnected; +} + ++ (void)controllerAdded:(IOHIDDeviceRef) device +{ + NSString *name = (__bridge NSString *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey)); + NSDictionary *hacks = hacksByName[name]; + if (!hacks) { + hacks = hacksByManufacturer[(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey))]; + } + NSArray *filters = hacks[JOYReportIDFilters]; + JOYController *controller = nil; + if (filters) { + controller = [[JOYMultiplayerController alloc] initWithDevice:device + reportIDFilters:filters + hacks:hacks]; + } + else { + controller = [[JOYController alloc] initWithDevice:device hacks:hacks]; + } + + [controllers setObject:controller forKey:[NSValue valueWithPointer:device]]; + + +} + ++ (void)controllerRemoved:(IOHIDDeviceRef) device +{ + [[controllers objectForKey:[NSValue valueWithPointer:device]] disconnected]; + [controllers removeObjectForKey:[NSValue valueWithPointer:device]]; +} + ++ (NSArray *)allControllers +{ + return exposedControllers; +} + ++ (void)load +{ +#include "ControllerConfiguration.inc" +} + ++(void)registerListener:(id)listener +{ + [listeners addObject:listener]; +} + ++(void)unregisterListener:(id)listener +{ + [listeners removeObject:listener]; +} + ++ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options +{ + axesEmulateButtons = [options[JOYAxesEmulateButtonsKey] boolValue]; + axes2DEmulateButtons = [options[JOYAxes2DEmulateButtonsKey] boolValue]; + hatsEmulateButtons = [options[JOYHatsEmulateButtonsKey] boolValue]; + + controllers = [NSMutableDictionary dictionary]; + exposedControllers = [NSMutableArray array]; + NSArray *array = @[ + CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick), + CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad), + CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController), + @{@kIOHIDDeviceUsagePageKey: @(kHIDPage_Game)}, + ]; + + listeners = [NSMutableSet set]; + static IOHIDManagerRef manager = nil; + if (manager) { + CFRelease(manager); // Stop the previous session + } + manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + + if (!manager) return; + if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone)) { + CFRelease(manager); + return; + } + + IOHIDManagerSetDeviceMatchingMultiple(manager, (__bridge CFArrayRef)array); + IOHIDManagerRegisterDeviceMatchingCallback(manager, HIDDeviceAdded, NULL); + IOHIDManagerRegisterDeviceRemovalCallback(manager, HIDDeviceRemoved, NULL); + IOHIDManagerScheduleWithRunLoop(manager, [runloop getCFRunLoop], kCFRunLoopDefaultMode); +} + +- (void)dealloc +{ + if (_device) { + CFRelease(_device); + _device = NULL; + } +} +@end diff --git a/JoyKit/JOYElement.h b/JoyKit/JOYElement.h new file mode 100644 index 0000000..0e917dd --- /dev/null +++ b/JoyKit/JOYElement.h @@ -0,0 +1,20 @@ +#import +#include + +@interface JOYElement : NSObject +- (instancetype)initWithElement:(IOHIDElementRef)element; +- (int32_t)value; +- (NSData *)dataValue; +- (IOReturn)setValue:(uint32_t)value; +- (IOReturn)setDataValue:(NSData *)value; +@property (readonly) uint16_t usage; +@property (readonly) uint16_t usagePage; +@property (readonly) uint32_t uniqueID; +@property int32_t min; +@property int32_t max; +@property (readonly) int32_t reportID; +@property (readonly) int32_t parentID; + +@end + + diff --git a/JoyKit/JOYElement.m b/JoyKit/JOYElement.m new file mode 100644 index 0000000..2432002 --- /dev/null +++ b/JoyKit/JOYElement.m @@ -0,0 +1,133 @@ +#import "JOYElement.h" +#include +#include + +@implementation JOYElement +{ + id _element; + IOHIDDeviceRef _device; + int32_t _min, _max; +} + +- (int32_t)min +{ + return MIN(_min, _max); +} + +- (int32_t)max +{ + return MAX(_max, _min); +} + +-(void)setMin:(int32_t)min +{ + _min = min; +} + +- (void)setMax:(int32_t)max +{ + _max = max; +} + +/* Ugly hack because IOHIDDeviceCopyMatchingElements is slow */ ++ (NSArray *) cookiesToSkipForDevice:(IOHIDDeviceRef)device +{ + id _device = (__bridge id)device; + NSMutableArray *ret = objc_getAssociatedObject(_device, _cmd); + if (ret) return ret; + + ret = [NSMutableArray array]; + NSArray *nones = CFBridgingRelease(IOHIDDeviceCopyMatchingElements(device, + (__bridge CFDictionaryRef)@{@(kIOHIDElementTypeKey): @(kIOHIDElementTypeInput_NULL)}, + 0)); + for (id none in nones) { + [ret addObject:@(IOHIDElementGetCookie((__bridge IOHIDElementRef)none))]; + } + objc_setAssociatedObject(_device, _cmd, ret, OBJC_ASSOCIATION_RETAIN); + return ret; +} + +- (instancetype)initWithElement:(IOHIDElementRef)element +{ + if ((self = [super init])) { + _element = (__bridge id)element; + _usage = IOHIDElementGetUsage(element); + _usagePage = IOHIDElementGetUsagePage(element); + _uniqueID = (uint32_t)IOHIDElementGetCookie(element); + _min = (int32_t) IOHIDElementGetLogicalMin(element); + _max = (int32_t) IOHIDElementGetLogicalMax(element); + _reportID = IOHIDElementGetReportID(element); + IOHIDElementRef parent = IOHIDElementGetParent(element); + _parentID = parent? (uint32_t)IOHIDElementGetCookie(parent) : -1; + _device = IOHIDElementGetDevice(element); + + /* Catalina added a new input type in a way that breaks cookie consistency across macOS versions, + we shall adjust our cookies to to compensate */ + unsigned cookieShift = 0, parentCookieShift = 0; + + for (NSNumber *none in [JOYElement cookiesToSkipForDevice:_device]) { + if (none.unsignedIntValue < _uniqueID) { + cookieShift++; + } + if (none.unsignedIntValue < (int32_t)_parentID) { + parentCookieShift++; + } + } + + _uniqueID -= cookieShift; + _parentID -= parentCookieShift; + } + return self; +} + +- (int32_t)value +{ + IOHIDValueRef value = NULL; + IOHIDDeviceGetValue(_device, (__bridge IOHIDElementRef)_element, &value); + if (!value) return 0; + CFRelease(CFRetain(value)); // For some reason, this is required to prevent leaks. + return (int32_t)IOHIDValueGetIntegerValue(value); +} + +- (NSData *)dataValue +{ + IOHIDValueRef value = NULL; + IOHIDDeviceGetValue(_device, (__bridge IOHIDElementRef)_element, &value); + if (!value) return 0; + CFRelease(CFRetain(value)); // For some reason, this is required to prevent leaks. + return [NSData dataWithBytes:IOHIDValueGetBytePtr(value) length:IOHIDValueGetLength(value)]; +} + +- (IOReturn)setValue:(uint32_t)value +{ + IOHIDValueRef ivalue = IOHIDValueCreateWithIntegerValue(NULL, (__bridge IOHIDElementRef)_element, 0, value); + IOReturn ret = IOHIDDeviceSetValue(_device, (__bridge IOHIDElementRef)_element, ivalue); + CFRelease(ivalue); + return ret; +} + +- (IOReturn)setDataValue:(NSData *)value +{ + IOHIDValueRef ivalue = IOHIDValueCreateWithBytes(NULL, (__bridge IOHIDElementRef)_element, 0, value.bytes, value.length); + IOReturn ret = IOHIDDeviceSetValue(_device, (__bridge IOHIDElementRef)_element, ivalue); + CFRelease(ivalue); + return ret; +} + +/* For use as a dictionary key */ + +- (NSUInteger)hash +{ + return self.uniqueID; +} + +- (BOOL)isEqual:(id)object +{ + return self->_element == object; +} + +- (id)copyWithZone:(nullable NSZone *)zone; +{ + return self; +} +@end diff --git a/JoyKit/JOYEmulatedButton.h b/JoyKit/JOYEmulatedButton.h new file mode 100644 index 0000000..491e0c7 --- /dev/null +++ b/JoyKit/JOYEmulatedButton.h @@ -0,0 +1,11 @@ +#import "JOYButton.h" +#import "JOYAxis.h" +#import "JOYAxes2D.h" +#import "JOYHat.h" + +@interface JOYEmulatedButton : JOYButton +- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +- (bool)updateStateFromAxis:(JOYAxis *)axis; +- (bool)updateStateFromAxes2D:(JOYAxes2D *)axes; +- (bool)updateStateFromHat:(JOYHat *)hat; +@end diff --git a/JoyKit/JOYEmulatedButton.m b/JoyKit/JOYEmulatedButton.m new file mode 100644 index 0000000..1ebed3a --- /dev/null +++ b/JoyKit/JOYEmulatedButton.m @@ -0,0 +1,91 @@ +#import "JOYEmulatedButton.h" + +@interface JOYButton () +{ + @public bool _state; +} +@end + +@implementation JOYEmulatedButton +{ + uint64_t _uniqueID; +} + +- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +{ + self = [super init]; + self.usage = usage; + _uniqueID = uniqueID; + + return self; +} + +- (uint64_t)uniqueID +{ + return _uniqueID; +} + +- (bool)updateStateFromAxis:(JOYAxis *)axis +{ + bool old = _state; + _state = [axis value] > 0.5; + return _state != old; +} + +- (bool)updateStateFromAxes2D:(JOYAxes2D *)axes +{ + bool old = _state; + if (axes.distance < 0.5) { + _state = false; + } + else { + unsigned direction = ((unsigned)round(axes.angle / 360 * 8)) & 7; + switch (self.usage) { + case JOYButtonUsageDPadLeft: + _state = direction >= 3 && direction <= 5; + break; + case JOYButtonUsageDPadRight: + _state = direction <= 1 || direction == 7; + break; + case JOYButtonUsageDPadUp: + _state = direction >= 5; + break; + case JOYButtonUsageDPadDown: + _state = direction <= 3 && direction >= 1; + break; + default: + break; + } + } + return _state != old; +} + +- (bool)updateStateFromHat:(JOYHat *)hat +{ + bool old = _state; + if (!hat.pressed) { + _state = false; + } + else { + unsigned direction = ((unsigned)round(hat.angle / 360 * 8)) & 7; + switch (self.usage) { + case JOYButtonUsageDPadLeft: + _state = direction >= 3 && direction <= 5; + break; + case JOYButtonUsageDPadRight: + _state = direction <= 1 || direction == 7; + break; + case JOYButtonUsageDPadUp: + _state = direction >= 5; + break; + case JOYButtonUsageDPadDown: + _state = direction <= 3 && direction >= 1; + break; + default: + break; + } + } + return _state != old; +} + +@end diff --git a/JoyKit/JOYFullReportElement.h b/JoyKit/JOYFullReportElement.h new file mode 100644 index 0000000..808644e --- /dev/null +++ b/JoyKit/JOYFullReportElement.h @@ -0,0 +1,10 @@ +#import +#include +#include "JOYElement.h" + +@interface JOYFullReportElement : JOYElement +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportID:(unsigned)reportID; +- (void)updateValue:(NSData *)value; +@end + + diff --git a/JoyKit/JOYFullReportElement.m b/JoyKit/JOYFullReportElement.m new file mode 100644 index 0000000..c8efb27 --- /dev/null +++ b/JoyKit/JOYFullReportElement.m @@ -0,0 +1,73 @@ +#import "JOYFullReportElement.h" +#include + +@implementation JOYFullReportElement +{ + IOHIDDeviceRef _device; + NSData *_data; + unsigned _reportID; + size_t _capacity; +} + +- (uint32_t)uniqueID +{ + return _reportID ^ 0xFFFF; +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportID:(unsigned)reportID +{ + if ((self = [super init])) { + _data = [[NSMutableData alloc] initWithLength:[(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxOutputReportSizeKey)) unsignedIntValue]]; + *(uint8_t *)(((NSMutableData *)_data).mutableBytes) = reportID; + _reportID = reportID; + _device = device; + } + return self; +} + +- (int32_t)value +{ + [self doesNotRecognizeSelector:_cmd]; + return 0; +} + +- (NSData *)dataValue +{ + return _data; +} + +- (IOReturn)setValue:(uint32_t)value +{ + [self doesNotRecognizeSelector:_cmd]; + return -1; +} + +- (IOReturn)setDataValue:(NSData *)value +{ + + [self updateValue:value]; + return IOHIDDeviceSetReport(_device, kIOHIDReportTypeOutput, _reportID, [_data bytes], [_data length]);; +} + +- (void)updateValue:(NSData *)value +{ + _data = [value copy]; +} + +/* For use as a dictionary key */ + +- (NSUInteger)hash +{ + return self.uniqueID; +} + +- (BOOL)isEqual:(id)object +{ + return self.uniqueID == self.uniqueID; +} + +- (id)copyWithZone:(nullable NSZone *)zone; +{ + return self; +} +@end diff --git a/JoyKit/JOYHat.h b/JoyKit/JOYHat.h new file mode 100644 index 0000000..05a5829 --- /dev/null +++ b/JoyKit/JOYHat.h @@ -0,0 +1,11 @@ +#import + +@interface JOYHat : NSObject +- (uint64_t)uniqueID; +- (double)angle; +- (unsigned)resolution; +@property (readonly, getter=isPressed) bool pressed; + +@end + + diff --git a/JoyKit/JOYHat.m b/JoyKit/JOYHat.m new file mode 100644 index 0000000..743e49c --- /dev/null +++ b/JoyKit/JOYHat.m @@ -0,0 +1,60 @@ +#import "JOYHat.h" +#import "JOYElement.h" + +@implementation JOYHat +{ + JOYElement *_element; + double _state; +} + +- (uint64_t)uniqueID +{ + return _element.uniqueID; +} + +- (NSString *)description +{ + if (self.isPressed) { + return [NSString stringWithFormat:@"<%@: %p (%llu); State: %f degrees>", self.className, self, self.uniqueID, self.angle]; + } + return [NSString stringWithFormat:@"<%@: %p (%llu); State: released>", self.className, self, self.uniqueID]; + +} + +- (instancetype)initWithElement:(JOYElement *)element +{ + self = [super init]; + if (!self) return self; + + _element = element; + + return self; +} + +- (bool)isPressed +{ + return _state >= 0 && _state < 360; +} + +- (double)angle +{ + if (self.isPressed) return fmod((_state + 270), 360); + return -1; +} + +- (unsigned)resolution +{ + return _element.max - _element.min + 1; +} + +- (bool)updateState +{ + unsigned state = ([_element value] - _element.min) * 360.0 / self.resolution; + if (_state != state) { + _state = state; + return true; + } + return false; +} + +@end diff --git a/JoyKit/JOYMultiplayerController.h b/JoyKit/JOYMultiplayerController.h new file mode 100644 index 0000000..44d7421 --- /dev/null +++ b/JoyKit/JOYMultiplayerController.h @@ -0,0 +1,8 @@ +#import "JOYController.h" +#include + +@interface JOYMultiplayerController : JOYController +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportIDFilters:(NSArray *>*) reportIDFilters hacks:hacks; +@end + + diff --git a/JoyKit/JOYMultiplayerController.m b/JoyKit/JOYMultiplayerController.m new file mode 100644 index 0000000..a31ae92 --- /dev/null +++ b/JoyKit/JOYMultiplayerController.m @@ -0,0 +1,50 @@ +#import "JOYMultiplayerController.h" + +@interface JOYController () +- (instancetype)initWithDevice:(IOHIDDeviceRef)device reportIDFilter:(NSArray *) filter serialSuffix:(NSString *)suffix hacks:(NSDictionary *)hacks; +- (void)elementChanged:(IOHIDElementRef) element toValue:(IOHIDValueRef) value; +- (void)disconnected; +- (void)sendReport:(NSData *)report; +@end + +@implementation JOYMultiplayerController +{ + NSMutableArray *_children; +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportIDFilters:(NSArray *>*) reportIDFilters hacks:(NSDictionary *)hacks; +{ + self = [super init]; + if (!self) return self; + + _children = [NSMutableArray array]; + + unsigned index = 1; + for (NSArray *filter in reportIDFilters) { + JOYController *controller = [[JOYController alloc] initWithDevice:device reportIDFilter:filter serialSuffix:[NSString stringWithFormat:@"%d", index] hacks:hacks]; + [_children addObject:controller]; + index++; + } + return self; +} + +- (void)elementChanged:(IOHIDElementRef) element toValue:(IOHIDValueRef) value +{ + for (JOYController *child in _children) { + [child elementChanged:element toValue:value]; + } +} + +- (void)disconnected +{ + for (JOYController *child in _children) { + [child disconnected]; + } +} + +- (void)sendReport:(NSData *)report +{ + [[_children firstObject] sendReport:report]; +} + +@end diff --git a/JoyKit/JOYSubElement.h b/JoyKit/JOYSubElement.h new file mode 100644 index 0000000..a13b5c7 --- /dev/null +++ b/JoyKit/JOYSubElement.h @@ -0,0 +1,14 @@ +#import "JOYElement.h" + +@interface JOYSubElement : JOYElement +- (instancetype)initWithRealElement:(JOYElement *)element + size:(size_t) size // in bits + offset:(size_t) offset // in bits + usagePage:(uint16_t)usagePage + usage:(uint16_t)usage + min:(int32_t)min + max:(int32_t)max; + +@end + + diff --git a/JoyKit/JOYSubElement.m b/JoyKit/JOYSubElement.m new file mode 100644 index 0000000..c94badc --- /dev/null +++ b/JoyKit/JOYSubElement.m @@ -0,0 +1,100 @@ +#import "JOYSubElement.h" + +@interface JOYElement () +{ + @public uint16_t _usage; + @public uint16_t _usagePage; + @public uint32_t _uniqueID; + @public int32_t _min; + @public int32_t _max; + @public int32_t _reportID; + @public int32_t _parentID; +} +@end + +@implementation JOYSubElement +{ + JOYElement *_parent; + size_t _size; // in bits + size_t _offset; // in bits +} + +- (instancetype)initWithRealElement:(JOYElement *)element + size:(size_t) size // in bits + offset:(size_t) offset // in bits + usagePage:(uint16_t)usagePage + usage:(uint16_t)usage + min:(int32_t)min + max:(int32_t)max +{ + if ((self = [super init])) { + _parent = element; + _size = size; + _offset = offset; + _usage = usage; + _usagePage = usagePage; + _uniqueID = (uint32_t)((_parent.uniqueID << 16) | offset); + _min = min; + _max = max; + _reportID = _parent.reportID; + _parentID = _parent.parentID; + } + return self; +} + +- (int32_t)value +{ + NSData *parentValue = [_parent dataValue]; + if (!parentValue) return 0; + if (_size > 32) return 0; + if (_size + (_offset % 8) > 32) return 0; + size_t parentLength = parentValue.length; + if (_size > parentLength * 8) return 0; + if (_size + _offset >= parentLength * 8) return 0; + const uint8_t *bytes = parentValue.bytes; + + uint8_t temp[4] = {0,}; + 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 (_max < _min) { + return _max + _min - ret; + } + + return ret; +} + +- (IOReturn)setValue: (uint32_t) value +{ + NSMutableData *dataValue = [[_parent dataValue] mutableCopy]; + if (!dataValue) return -1; + if (_size > 32) return -1; + if (_size + (_offset % 8) > 32) return -1; + size_t parentLength = dataValue.length; + if (_size > parentLength * 8) return -1; + if (_size + _offset >= parentLength * 8) return -1; + uint8_t *bytes = dataValue.mutableBytes; + + uint8_t temp[4] = {0,}; + memcpy(temp, bytes + _offset / 8, (_offset + _size - 1) / 8 - _offset / 8 + 1); + (*(uint32_t *)temp) &= ~((1 << (_size - 1)) << (_offset % 8)); + (*(uint32_t *)temp) |= (value) << (_offset % 8); + memcpy(bytes + _offset / 8, temp, (_offset + _size - 1) / 8 - _offset / 8 + 1); + return [_parent setDataValue:dataValue]; +} + +- (NSData *)dataValue +{ + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (IOReturn)setDataValue:(NSData *)data +{ + [self doesNotRecognizeSelector:_cmd]; + return -1; +} + + +@end diff --git a/JoyKit/JoyKit.h b/JoyKit/JoyKit.h new file mode 100644 index 0000000..d56b505 --- /dev/null +++ b/JoyKit/JoyKit.h @@ -0,0 +1,6 @@ +#ifndef JoyKit_h +#define JoyKit_h + +#include "JOYController.h" + +#endif diff --git a/LICENSE b/LICENSE index 63c6787..17619e9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2015-2018 Lior Halphon +Copyright (c) 2015-2020 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 old mode 100755 new mode 100644 index fe0ff81..9cacef5 --- a/Makefile +++ b/Makefile @@ -15,8 +15,15 @@ endif ifeq ($(PLATFORM),windows32) _ := $(shell chcp 65001) +EXESUFFIX:=.exe +NATIVE_CC = clang -IWindows -Wno-deprecated-declarations +else +EXESUFFIX:= +NATIVE_CC := cc endif +PB12_COMPRESS := build/pb12$(EXESUFFIX) + ifeq ($(PLATFORM),Darwin) DEFAULT := cocoa else @@ -29,9 +36,10 @@ ifeq ($(MAKECMDGOALS),) MAKECMDGOALS := $(DEFAULT) endif -VERSION := 0.11.1 +VERSION := 0.13.2 export VERSION CONF ?= debug +SDL_AUDIO_DRIVER ?= sdl BIN := build/bin OBJ := build/obj @@ -44,8 +52,15 @@ endif # Set tools # Use clang if it's available. +ifeq ($(origin CC),default) ifneq (, $(shell which clang)) -CC := clang +CC := clang +endif +endif + +# Find libraries with pkg-config if available. +ifneq (, $(shell which pkg-config)) +PKG_CONFIG := pkg-config endif ifeq ($(PLATFORM),windows32) @@ -63,22 +78,66 @@ endif # Set compilation and linkage flags based on target, platform and configuration -CFLAGS += -Werror -Wall -Wno-unknown-warning-option -Wno-multichar -Wno-int-in-bool-context -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES -SDL_LDFLAGS := -lSDL2 -lGL +OPEN_DIALOG = OpenDialog/gtk.c +NULL := /dev/null + ifeq ($(PLATFORM),windows32) -CFLAGS += -IWindows -LDFLAGS += -lmsvcrt -lSDL2main -Wl,/MANIFESTFILE:NUL -SDL_LDFLAGS := -lSDL2 -lopengl32 -else -LDFLAGS += -lc -lm +OPEN_DIALOG = OpenDialog/windows.c +NULL := NUL endif ifeq ($(PLATFORM),Darwin) -SYSROOT := $(shell xcodebuild -sdk macosx -version Path 2> /dev/null) -CFLAGS += -F/Library/Frameworks -OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT) -mmacosx-version-min=10.9 -LDFLAGS += -framework AppKit -framework PreferencePanes -framework Carbon -framework QuartzCore -weak_framework Metal -weak_framework MetalKit -SDL_LDFLAGS := -F/Library/Frameworks -framework SDL2 -framework OpenGL +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 + +# 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) +WARNINGS += -Wpartial-availability +endif + +CFLAGS += $(WARNINGS) + +CFLAGS += -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES + +ifeq (,$(PKG_CONFIG)) +SDL_CFLAGS := $(shell sdl2-config --cflags) +SDL_LDFLAGS := $(shell sdl2-config --libs) +else +SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2) +SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) +endif +ifeq (,$(PKG_CONFIG)) +GL_LDFLAGS := -lGL +else +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 +SDL_LDFLAGS := -lSDL2 +GL_LDFLAGS := -lopengl32 +else +LDFLAGS += -lc -lm -ldl +endif + +ifeq ($(PLATFORM),Darwin) +SYSROOT := $(shell xcodebuild -sdk macosx -version Path 2> $(NULL)) +ifeq ($(SYSROOT),) +SYSROOT := /Library/Developer/CommandLineTools/SDKs/$(shell ls /Library/Developer/CommandLineTools/SDKs/ | grep 10 | tail -n 1) +endif +ifeq ($(SYSROOT),/Library/Developer/CommandLineTools/SDKs/) +$(error Could not find a macOS SDK) +endif + +CFLAGS += -F/Library/Frameworks -mmacosx-version-min=10.9 +OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT) +LDFLAGS += -framework AppKit -framework PreferencePanes -framework Carbon -framework QuartzCore -weak_framework Metal -weak_framework MetalKit -mmacosx-version-min=10.9 +GL_LDFLAGS := -framework OpenGL endif CFLAGS += -Wno-deprecated-declarations ifeq ($(PLATFORM),windows32) @@ -90,10 +149,17 @@ ifeq ($(CONF),debug) CFLAGS += -g else ifeq ($(CONF), release) CFLAGS += -O3 -DNDEBUG +STRIP := strip +ifeq ($(PLATFORM),Darwin) +LDFLAGS += -Wl,-exported_symbols_list,$(NULL) +STRIP := -@true +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") endif @@ -119,12 +185,12 @@ all: cocoa sdl tester libretro # Get a list of our source files and their respective object file targets CORE_SOURCES := $(shell ls Core/*.c) -SDL_SOURCES := $(shell ls SDL/*.c) +SDL_SOURCES := $(shell ls SDL/*.c) $(OPEN_DIALOG) SDL/audio/$(SDL_AUDIO_DRIVER).c TESTER_SOURCES := $(shell ls Tester/*.c) GNOME_THUMBNAILER_SOURCES := $(shell ls GnomeThumbnailer/*.c) ifeq ($(PLATFORM),Darwin) -COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) +COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) $(shell ls JoyKit/*.m) QUICKLOOK_SOURCES := $(shell ls QuickLook/*.m) $(shell ls QuickLook/*.c) endif @@ -154,6 +220,10 @@ ifneq ($(filter $(MAKECMDGOALS),cocoa),) endif endif +$(OBJ)/SDL/%.dep: SDL/% + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -MT $(OBJ)/$^.o -M $^ -c -o $@ + $(OBJ)/%.dep: % -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) -MT $(OBJ)/$^.o -M $^ -c -o $@ @@ -164,6 +234,10 @@ $(OBJ)/Core/%.c.o: Core/%.c -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) -DGB_INTERNAL -c $< -o $@ +$(OBJ)/SDL/%.c.o: SDL/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ + $(OBJ)/%.c.o: %.c -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) -c $< -o $@ @@ -205,7 +279,7 @@ $(BIN)/SameBoy.app/Contents/MacOS/SameBoy: $(CORE_OBJECTS) $(COCOA_OBJECTS) -@$(MKDIR) -p $(dir $@) $(CC) $^ -o $@ $(LDFLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework IOKit ifeq ($(CONF), release) - true $@ + $(STRIP) $@ endif $(BIN)/SameBoy.app/Contents/Resources/Base.lproj/%.nib: Cocoa/%.xib @@ -241,9 +315,9 @@ $(BIN)/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin: $(BIN)/BootROMs # Unix versions build only one binary $(BIN)/SDL/sameboy: $(CORE_OBJECTS) $(SDL_OBJECTS) -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) + $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) ifeq ($(CONF), release) - true $@ + $(STRIP) $@ endif $(OBJ)/GnomeThumbnailer/cgb_boot_fast.o: $(BIN)/BootROMs/cgb_boot_fast.bin @@ -263,18 +337,15 @@ $(BIN)/GnomeThumbnailer/SameBoy-thumbnailer: $(CORE_OBJECTS) $(GNOME_THUMBNAILER $(OBJ)/GnomeThumbnailer/UniversalCartridgeTemplate.o -@$(MKDIR) -p $(dir $@) $(CC) $(CORE_OBJECTS) $(GNOME_THUMBNAILER_OBJECTS) $(OBJ)/GnomeThumbnailer/cgb_boot_fast.o $(OBJ)/GnomeThumbnailer/CartridgeTemplate.o $(OBJ)/GnomeThumbnailer/ColorCartridgeTemplate.o $(OBJ)/GnomeThumbnailer/UniversalCartridgeTemplate.o -o $@ $(LDFLAGS) -ifeq ($(CONF), release) - true $@ -endif # Windows version builds two, one with a conole and one without it $(BIN)/SDL/sameboy.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/resources.o -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) -Wl,/subsystem:windows + $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) -Wl,/subsystem:windows $(BIN)/SDL/sameboy_debugger.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/resources.o -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) -Wl,/subsystem:console + $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) -Wl,/subsystem:console ifneq ($(USE_WINDRES),) $(OBJ)/%.o: %.rc @@ -300,14 +371,18 @@ $(BIN)/tester/sameboy_tester: $(CORE_OBJECTS) $(TESTER_OBJECTS) -@$(MKDIR) -p $(dir $@) $(CC) $^ -o $@ $(LDFLAGS) ifeq ($(CONF), release) - true $@ + $(STRIP) $@ endif $(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) -@$(MKDIR) -p $(dir $@) $(CC) $^ -o $@ $(LDFLAGS) -Wl,/subsystem:console -$(BIN)/SDL/%.bin $(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin +$(BIN)/SDL/%.bin: $(BOOTROMS_DIR)/%.bin + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +$(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ @@ -333,19 +408,33 @@ $(BIN)/SDL/Shaders: Shaders # Boot ROMs -$(BIN)/BootROMs/%.bin: BootROMs/%.asm +$(OBJ)/%.2bpp: %.png -@$(MKDIR) -p $(dir $@) - cd BootROMs && rgbasm -o ../$@.tmp ../$< + rgbgfx -h -u -o $@ $< + +$(OBJ)/BootROMs/SameBoyLogo.pb12: $(OBJ)/BootROMs/SameBoyLogo.2bpp $(PB12_COMPRESS) + $(realpath $(PB12_COMPRESS)) < $< > $@ + +$(PB12_COMPRESS): BootROMs/pb12.c + $(NATIVE_CC) -Wall -Werror $< -o $@ + +$(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 + +$(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.pb12 + -@$(MKDIR) -p $(dir $@) + rgbasm -i $(OBJ)/BootROMs/ -i BootROMs/ -o $@.tmp $< rgblink -o $@.tmp2 $@.tmp - head -c $(if $(findstring dmg,$@)$(findstring sgb,$@), 256, 2304) $@.tmp2 > $@ + dd if=$@.tmp2 of=$@ count=1 bs=$(if $(findstring dmg,$@)$(findstring sgb,$@),256,2304) 2> $(NULL) @rm $@.tmp $@.tmp2 # Libretro Core (uses its own build system) libretro: - $(MAKE) -C libretro + CFLAGS="$(WARNINGS)" $(MAKE) -C libretro # Clean clean: rm -rf build -.PHONY: libretro +.PHONY: libretro tester diff --git a/Misc/registers.sym b/Misc/registers.sym index 3bc95bb..3b31b74 100644 --- a/Misc/registers.sym +++ b/Misc/registers.sym @@ -41,10 +41,10 @@ 00:FF49 IO_OBP1 00:FF4A IO_WY 00:FF4B IO_WX -00:FF4C IO_DMG_EMULATION +00:FF4C IO_KEY0 00:FF4D IO_KEY1 00:FF4F IO_VBK -00:FF50 IO_BIOS +00:FF50 IO_BANK 00:FF51 IO_HDMA1 00:FF52 IO_HDMA2 00:FF53 IO_HDMA3 @@ -55,7 +55,7 @@ 00:FF69 IO_BGPD 00:FF6A IO_OBPI 00:FF6B IO_OBPD -00:FF6C IO_DMG_EMULATION_INDICATION +00:FF6C IO_OPRI 00:FF70 IO_SVBK 00:FF72 IO_UNKNOWN2 00:FF73 IO_UNKNOWN3 @@ -64,4 +64,4 @@ 00:FF76 IO_PCM_12 00:FF77 IO_PCM_34 00:FF7F IO_UNKNOWN8 -00:FFFF IO_IE \ No newline at end of file +00:FFFF IO_IE diff --git a/OpenDialog/cocoa.m b/OpenDialog/cocoa.m new file mode 100644 index 0000000..76b9606 --- /dev/null +++ b/OpenDialog/cocoa.m @@ -0,0 +1,20 @@ +#import +#include "open_dialog.h" + + +char *do_open_rom_dialog(void) +{ + @autoreleasepool { + NSWindow *key = [NSApp keyWindow]; + NSOpenPanel *dialog = [NSOpenPanel openPanel]; + dialog.title = @"Open ROM"; + dialog.allowedFileTypes = @[@"gb", @"gbc", @"sgb", @"isx"]; + [dialog runModal]; + [key makeKeyAndOrderFront:nil]; + NSString *ret = [[[dialog URLs] firstObject] path]; + if (ret) { + return strdup(ret.UTF8String); + } + return NULL; + } +} diff --git a/OpenDialog/gtk.c b/OpenDialog/gtk.c new file mode 100644 index 0000000..5b1caa3 --- /dev/null +++ b/OpenDialog/gtk.c @@ -0,0 +1,113 @@ +#include "open_dialog.h" +#include +#include +#include +#include +#include + +#define GTK_FILE_CHOOSER_ACTION_OPEN 0 +#define GTK_RESPONSE_ACCEPT -3 +#define GTK_RESPONSE_CANCEL -6 + + +void *_gtk_file_chooser_dialog_new (const char *title, + void *parent, + int action, + const char *first_button_text, + ...); +bool _gtk_init_check (int *argc, char ***argv); +int _gtk_dialog_run(void *); +void _g_free(void *); +void _gtk_widget_destroy(void *); +char *_gtk_file_chooser_get_filename(void *); +void _g_log_set_default_handler (void *function, void *data); +void *_gtk_file_filter_new(void); +void _gtk_file_filter_add_pattern(void *filter, const char *pattern); +void _gtk_file_filter_set_name(void *filter, const char *name); +void _gtk_file_chooser_add_filter(void *dialog, void *filter); +void _gtk_main_iteration(void); +bool _gtk_events_pending(void); + + +#define LAZY(symbol) static typeof(_##symbol) *symbol = NULL;\ +if (symbol == NULL) symbol = dlsym(handle, #symbol);\ +if (symbol == NULL) goto lazy_error +#define TRY_DLOPEN(name) handle = handle? handle : dlopen(name, RTLD_NOW) + +void nop(){} + +char *do_open_rom_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("Open ROM", + 0, + GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Open", GTK_RESPONSE_ACCEPT, + NULL ); + + + void *filter = gtk_file_filter_new(); + gtk_file_filter_add_pattern(filter, "*.gb"); + gtk_file_filter_add_pattern(filter, "*.gbc"); + gtk_file_filter_add_pattern(filter, "*.sgb"); + gtk_file_filter_add_pattern(filter, "*.isx"); + gtk_file_filter_set_name(filter, "Game Boy ROMs"); + gtk_file_chooser_add_filter(dialog, filter); + + 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 new file mode 100644 index 0000000..85e5721 --- /dev/null +++ b/OpenDialog/open_dialog.h @@ -0,0 +1,6 @@ +#ifndef open_rom_h +#define open_rom_h + +char *do_open_rom_dialog(void); + +#endif /* open_rom_h */ diff --git a/OpenDialog/windows.c b/OpenDialog/windows.c new file mode 100644 index 0000000..52e281d --- /dev/null +++ b/OpenDialog/windows.c @@ -0,0 +1,27 @@ +#include +#include "open_dialog.h" + +char *do_open_rom_dialog(void) +{ + OPENFILENAMEW dialog; + wchar_t filename[MAX_PATH] = {0}; + + memset(&dialog, 0, sizeof(dialog)); + dialog.lStructSize = sizeof(dialog); + dialog.lpstrFile = filename; + dialog.nMaxFile = sizeof(filename); + dialog.lpstrFilter = L"Game Boy ROMs\0*.gb;*.gbc;*.sgb;*.isx\0All files\0*.*\0\0"; + dialog.nFilterIndex = 1; + dialog.lpstrFileTitle = NULL; + dialog.nMaxFileTitle = 0; + dialog.lpstrInitialDir = NULL; + dialog.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; + + if (GetOpenFileNameW(&dialog) == TRUE) { + char *ret = malloc(MAX_PATH * 4); + WideCharToMultiByte(CP_UTF8, 0, filename, sizeof(filename), ret, MAX_PATH * 4, NULL, NULL); + return ret; + } + + return NULL; +} diff --git a/QuickLook/Info.plist b/QuickLook/Info.plist index 051e26c..b01aae1 100644 --- a/QuickLook/Info.plist +++ b/QuickLook/Info.plist @@ -13,6 +13,7 @@ com.github.liji32.sameboy.gb com.github.liji32.sameboy.gbc + com.github.liji32.sameboy.isx @@ -47,7 +48,7 @@ CFPlugInUnloadFunction NSHumanReadableCopyright - Copyright © 2015-2018 Lior Halphon + Copyright © 2015-2020 Lior Halphon QLNeedsToBeRunInMainThread QLPreviewHeight diff --git a/QuickLook/exports.sym b/QuickLook/exports.sym index 778b203..f979687 100644 --- a/QuickLook/exports.sym +++ b/QuickLook/exports.sym @@ -1,3 +1,3 @@ _DeallocQuickLookGeneratorPluginType _QuickLookGeneratorQueryInterface -_QuickLookGeneratorPluginFactory \ No newline at end of file +_QuickLookGeneratorPluginFactory diff --git a/QuickLook/generator.m b/QuickLook/generator.m index 1aa0087..c3c13dc 100644 --- a/QuickLook/generator.m +++ b/QuickLook/generator.m @@ -79,7 +79,7 @@ static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) } /* Mask it with the template (The middle part of the template image is transparent) */ - [effectiveTemplate drawInRect:(NSRect){{0,0},template.size}]; + [effectiveTemplate drawInRect:(NSRect){{0, 0}, template.size}]; } CGColorSpaceRelease(colorSpaceRef); diff --git a/QuickLook/get_image_for_rom.c b/QuickLook/get_image_for_rom.c index b1b727b..a4da508 100755 --- a/QuickLook/get_image_for_rom.c +++ b/QuickLook/get_image_for_rom.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "get_image_for_rom.h" @@ -50,8 +51,24 @@ int get_image_for_rom_common(GB_gameboy_t gb, const char *filename, uint32_t *ou GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_async_input_callback(&gb, async_input_callback); GB_set_log_callback(&gb, log_callback); - - if (GB_load_rom(&gb, filename)) { + GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_EMULATE_HARDWARE); + + size_t length = strlen(filename); + char extension[4] = {0,}; + if (length > 4) { + if (filename[length - 4] == '.') { + extension[0] = tolower(filename[length - 3]); + extension[1] = tolower(filename[length - 2]); + extension[2] = tolower(filename[length - 1]); + } + } + if (strcmp(extension, "isx") == 0) { + if (GB_load_isx(&gb, filename)) { + GB_free(&gb); + return 1; + } + } + else if (GB_load_rom(&gb, filename)) { GB_free(&gb); return 1; } diff --git a/QuickLook/main.c b/QuickLook/main.c index 8566e32..1d1676a 100644 --- a/QuickLook/main.c +++ b/QuickLook/main.c @@ -43,8 +43,8 @@ typedef struct __QuickLookGeneratorPluginType QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFactoryID); void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInstance); -HRESULT QuickLookGeneratorQueryInterface(void *thisInstance,REFIID iid,LPVOID *ppv); -void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID); +HRESULT QuickLookGeneratorQueryInterface(void *thisInstance, REFIID iid, LPVOID *ppv); +void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID); ULONG QuickLookGeneratorPluginAddRef(void *thisInstance); ULONG QuickLookGeneratorPluginRelease(void *thisInstance); @@ -77,11 +77,11 @@ QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFact QuickLookGeneratorPluginType *theNewInstance; theNewInstance = (QuickLookGeneratorPluginType *)malloc(sizeof(QuickLookGeneratorPluginType)); - memset(theNewInstance,0,sizeof(QuickLookGeneratorPluginType)); + memset(theNewInstance, 0, sizeof(QuickLookGeneratorPluginType)); /* Point to the function table Malloc enough to store the stuff and copy the filler from myInterfaceFtbl over */ theNewInstance->conduitInterface = malloc(sizeof(QLGeneratorInterfaceStruct)); - memcpy(theNewInstance->conduitInterface,&myInterfaceFtbl,sizeof(QLGeneratorInterfaceStruct)); + memcpy(theNewInstance->conduitInterface,&myInterfaceFtbl, sizeof(QLGeneratorInterfaceStruct)); /* Retain and keep an open instance refcount for each factory. */ theNewInstance->factoryID = CFRetain(inFactoryID); @@ -110,7 +110,7 @@ void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInsta /* Free the instance structure */ free(thisInstance); - if (theFactoryID){ + if (theFactoryID) { CFPlugInRemoveInstanceForFactory(theFactoryID); CFRelease(theFactoryID); } @@ -121,13 +121,13 @@ void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInsta // ----------------------------------------------------------------------------- // Implementation of the IUnknown QueryInterface function. // -HRESULT QuickLookGeneratorQueryInterface(void *thisInstance,REFIID iid,LPVOID *ppv) +HRESULT QuickLookGeneratorQueryInterface(void *thisInstance, REFIID iid, LPVOID *ppv) { CFUUIDRef interfaceID; - interfaceID = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault,iid); + interfaceID = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, iid); - if (CFEqual(interfaceID,kQLGeneratorCallbacksInterfaceID)){ + if (CFEqual(interfaceID, kQLGeneratorCallbacksInterfaceID)) { /* If the Right interface was requested, bump the ref count, * set the ppv parameter equal to the instance, and * return good status. @@ -138,7 +138,8 @@ HRESULT QuickLookGeneratorQueryInterface(void *thisInstance,REFIID iid,LPVOID *p *ppv = thisInstance; CFRelease(interfaceID); return S_OK; - }else{ + } + else { /* Requested interface unknown, bail with error. */ *ppv = NULL; CFRelease(interfaceID); @@ -168,10 +169,11 @@ ULONG QuickLookGeneratorPluginAddRef(void *thisInstance) ULONG QuickLookGeneratorPluginRelease(void *thisInstance) { ((QuickLookGeneratorPluginType*)thisInstance)->refCount -= 1; - if (((QuickLookGeneratorPluginType*)thisInstance)->refCount == 0){ + if (((QuickLookGeneratorPluginType*)thisInstance)->refCount == 0) { DeallocQuickLookGeneratorPluginType((QuickLookGeneratorPluginType*)thisInstance ); return 0; - }else{ + } + else { return ((QuickLookGeneratorPluginType*) thisInstance )->refCount; } } @@ -179,7 +181,7 @@ ULONG QuickLookGeneratorPluginRelease(void *thisInstance) // ----------------------------------------------------------------------------- // QuickLookGeneratorPluginFactory // ----------------------------------------------------------------------------- -void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID) +void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID) { QuickLookGeneratorPluginType *result; CFUUIDRef uuid; @@ -187,8 +189,8 @@ void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID) /* If correct type is being requested, allocate an * instance of kQLGeneratorTypeID and return the IUnknown interface. */ - if (CFEqual(typeID,kQLGeneratorTypeID)){ - uuid = CFUUIDCreateFromString(kCFAllocatorDefault,CFSTR(PLUGIN_ID)); + if (CFEqual(typeID, kQLGeneratorTypeID)) { + uuid = CFUUIDCreateFromString(kCFAllocatorDefault, CFSTR(PLUGIN_ID)); result = AllocQuickLookGeneratorPluginType(uuid); CFRelease(uuid); return result; diff --git a/README.md b/README.md index 91e0bf6..d626bbe 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ SameBoy requires the following tools and libraries to build: * clang * make * Cocoa port: OS X SDK and Xcode command line tools - * SDL port: SDL2.framework (OS X) or libsdl2 (Other platforms) + * SDL port: libsdl2 * [rgbds](https://github.com/bentley/rgbds/releases/), for boot ROM compilation On Windows, SameBoy also requires: diff --git a/SDL/audio/audio.h b/SDL/audio/audio.h new file mode 100644 index 0000000..acaa011 --- /dev/null +++ b/SDL/audio/audio.h @@ -0,0 +1,16 @@ +#ifndef sdl_audio_h +#define sdl_audio_h + +#include +#include +#include + +bool GB_audio_is_playing(void); +void GB_audio_set_paused(bool paused); +void GB_audio_clear_queue(void); +unsigned GB_audio_get_frequency(void); +size_t GB_audio_get_queue_length(void); +void GB_audio_queue_sample(GB_sample_t *sample); +void GB_audio_init(void); + +#endif /* sdl_audio_h */ diff --git a/SDL/audio/sdl.c b/SDL/audio/sdl.c new file mode 100644 index 0000000..12ee69a --- /dev/null +++ b/SDL/audio/sdl.c @@ -0,0 +1,96 @@ +#include "audio.h" +#include + +#ifndef _WIN32 +#define AUDIO_FREQUENCY 96000 +#include +#else +#include +/* Windows (well, at least my VM) can't handle 96KHz sound well :( */ + +/* felsqualle says: For SDL 2.0.6+ using the WASAPI driver, the highest freq. + we can get is 48000. 96000 also works, but always has some faint crackling in + the audio, no matter how high or low I set the buffer length... + Not quite satisfied with that solution, because acc. to SDL2 docs, + 96k + WASAPI *should* work. */ + +#define AUDIO_FREQUENCY 48000 +#endif + +/* Compatibility with older SDL versions */ +#ifndef SDL_AUDIO_ALLOW_SAMPLES_CHANGE +#define SDL_AUDIO_ALLOW_SAMPLES_CHANGE 0 +#endif + +static SDL_AudioDeviceID device_id; +static SDL_AudioSpec want_aspec, have_aspec; + +#define AUDIO_BUFFER_SIZE 512 +static unsigned buffer_pos = 0; +static GB_sample_t audio_buffer[AUDIO_BUFFER_SIZE]; + +bool GB_audio_is_playing(void) +{ + return SDL_GetAudioDeviceStatus(device_id) == SDL_AUDIO_PLAYING; +} + +void GB_audio_set_paused(bool paused) +{ + GB_audio_clear_queue(); + SDL_PauseAudioDevice(device_id, paused); +} + +void GB_audio_clear_queue(void) +{ + SDL_ClearQueuedAudio(device_id); +} + +unsigned GB_audio_get_frequency(void) +{ + return have_aspec.freq; +} + +size_t GB_audio_get_queue_length(void) +{ + return SDL_GetQueuedAudioSize(device_id); +} + +void GB_audio_queue_sample(GB_sample_t *sample) +{ + audio_buffer[buffer_pos++] = *sample; + + if (buffer_pos == AUDIO_BUFFER_SIZE) { + buffer_pos = 0; + SDL_QueueAudio(device_id, (const void *)audio_buffer, sizeof(audio_buffer)); + } +} + +void GB_audio_init(void) +{ + /* Configure Audio */ + memset(&want_aspec, 0, sizeof(want_aspec)); + want_aspec.freq = AUDIO_FREQUENCY; + want_aspec.format = AUDIO_S16SYS; + want_aspec.channels = 2; + want_aspec.samples = 512; + + SDL_version _sdl_version; + SDL_GetVersion(&_sdl_version); + unsigned sdl_version = _sdl_version.major * 1000 + _sdl_version.minor * 100 + _sdl_version.patch; + +#ifndef _WIN32 + /* SDL 2.0.5 on macOS and Linux introduced a bug where certain combinations of buffer lengths and frequencies + fail to produce audio correctly. */ + if (sdl_version >= 2005) { + want_aspec.samples = 2048; + } +#else + if (sdl_version < 2006) { + /* Since WASAPI audio was introduced in SDL 2.0.6, we have to lower the audio frequency + to 44100 because otherwise we would get garbled audio output.*/ + want_aspec.freq = 44100; + } +#endif + + device_id = SDL_OpenAudioDevice(0, 0, &want_aspec, &have_aspec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_SAMPLES_CHANGE); +} diff --git a/SDL/gui.c b/SDL/gui.c index 4de76f5..81a9e42 100644 --- a/SDL/gui.c +++ b/SDL/gui.c @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -25,13 +26,14 @@ unsigned command_parameter; #endif shader_t shader; -SDL_Rect rect; +static SDL_Rect rect; +static unsigned factor; void render_texture(void *pixels, void *previous) { if (renderer) { if (pixels) { - SDL_UpdateTexture(texture, NULL, pixels, 160 * sizeof (uint32_t)); + SDL_UpdateTexture(texture, NULL, pixels, GB_get_screen_width(&gb) * sizeof (uint32_t)); } SDL_RenderClear(renderer); SDL_RenderCopy(renderer, texture, NULL, NULL); @@ -44,7 +46,22 @@ void render_texture(void *pixels, void *previous) } glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); - render_bitmap_with_shader(&shader, _pixels, previous, rect.x, rect.y, rect.w, rect.h); + GB_frame_blending_mode_t mode = configuration.blending_mode; + if (!previous) { + mode = GB_FRAME_BLENDING_MODE_DISABLED; + } + else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) { + if (GB_is_sgb(&gb)) { + mode = GB_FRAME_BLENDING_MODE_SIMPLE; + } + else { + mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; + } + } + render_bitmap_with_shader(&shader, _pixels, previous, + GB_get_screen_width(&gb), GB_get_screen_height(&gb), + rect.x, rect.y, rect.w, rect.h, + mode); SDL_GL_SwapWindow(window); } } @@ -87,18 +104,20 @@ configuration_t configuration = .color_correction_mode = GB_COLOR_CORRECTION_EMULATE_HARDWARE, .highpass_mode = GB_HIGHPASS_ACCURATE, .scaling_mode = GB_SDL_SCALING_INTEGER_FACTOR, - .blend_frames = true, + .blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE, .rewind_length = 60 * 2, - .model = MODEL_CGB + .model = MODEL_CGB, + .volume = 100, + .rumble_mode = GB_RUMBLE_ALL_GAMES, }; -static const char *help[] ={ -"Drop a GB or GBC ROM\n" -"file to play.\n" +static const char *help[] = { +"Drop a ROM to play.\n" "\n" "Keyboard Shortcuts:\n" " Open Menu: Escape\n" +" Open ROM: " MODIFIER_NAME "+O\n" " Reset: " MODIFIER_NAME "+R\n" " Pause: " MODIFIER_NAME "+P\n" " Save state: " MODIFIER_NAME "+(0-9)\n" @@ -116,12 +135,16 @@ void update_viewport(void) { int win_width, win_height; SDL_GL_GetDrawableSize(window, &win_width, &win_height); - double x_factor = win_width / 160.0; - double y_factor = win_height / 144.0; + int logical_width, logical_height; + SDL_GetWindowSize(window, &logical_width, &logical_height); + factor = win_width / logical_width; + + double x_factor = win_width / (double) GB_get_screen_width(&gb); + double y_factor = win_height / (double) GB_get_screen_height(&gb); if (configuration.scaling_mode == GB_SDL_SCALING_INTEGER_FACTOR) { - x_factor = (int)(x_factor); - y_factor = (int)(y_factor); + x_factor = (unsigned)(x_factor); + y_factor = (unsigned)(y_factor); } if (configuration.scaling_mode != GB_SDL_SCALING_ENTIRE_WINDOW) { @@ -133,8 +156,8 @@ void update_viewport(void) } } - unsigned new_width = x_factor * 160; - unsigned new_height = y_factor * 144; + unsigned new_width = x_factor * GB_get_screen_width(&gb); + unsigned new_height = y_factor * GB_get_screen_height(&gb); rect = (SDL_Rect){(win_width - new_width) / 2, (win_height - new_height) /2, new_width, new_height}; @@ -148,7 +171,7 @@ void update_viewport(void) } /* Does NOT check for bounds! */ -static void draw_char(uint32_t *buffer, unsigned char ch, uint32_t color) +static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigned char ch, uint32_t color) { if (ch < ' ' || ch > font_max) { ch = '?'; @@ -163,13 +186,16 @@ static void draw_char(uint32_t *buffer, unsigned char ch, uint32_t color) } buffer++; } - buffer += 160 - GLYPH_WIDTH; + buffer += width - GLYPH_WIDTH; } } -static void draw_unbordered_text(uint32_t *buffer, unsigned x, unsigned y, const char *string, uint32_t color) +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) { + y -= scroll; unsigned orig_x = x; + unsigned y_offset = (GB_get_screen_height(&gb) - 144) / 2; while (*string) { if (*string == '\n') { x = orig_x; @@ -178,23 +204,23 @@ static void draw_unbordered_text(uint32_t *buffer, unsigned x, unsigned y, const continue; } - if (x > 160 - GLYPH_WIDTH || y == 0 || y > 144 - GLYPH_HEIGHT) { + if (x > width - GLYPH_WIDTH || y == 0 || y - y_offset > 144 - GLYPH_HEIGHT) { break; } - draw_char(&buffer[x + 160 * y], *string, color); + draw_char(&buffer[x + width * y], width, height, *string, color); x += GLYPH_WIDTH; string++; } } -static void draw_text(uint32_t *buffer, unsigned x, unsigned y, const char *string, uint32_t color, uint32_t border) +static void draw_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, unsigned y, const char *string, uint32_t color, uint32_t border) { - draw_unbordered_text(buffer, x - 1, y, string, border); - draw_unbordered_text(buffer, x + 1, y, string, border); - draw_unbordered_text(buffer, x, y - 1, string, border); - draw_unbordered_text(buffer, x, y + 1, string, border); - draw_unbordered_text(buffer, x, y, string, color); + 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); } enum decoration { @@ -203,17 +229,17 @@ enum decoration { DECORATION_ARROWS, }; -static void draw_text_centered(uint32_t *buffer, unsigned y, const char *string, uint32_t color, uint32_t border, enum decoration 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 = 160 / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2; - draw_text(buffer, x, y, string, color, border); + unsigned x = width / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2; + draw_text(buffer, width, height, x, y, string, color, border); switch (decoration) { case DECORATION_SELECTION: - draw_text(buffer, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border); + draw_text(buffer, width, height, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border); break; case DECORATION_ARROWS: - draw_text(buffer, x - GLYPH_WIDTH, y, LEFT_ARROW_STRING, color, border); - draw_text(buffer, 160 - x, y, RIGHT_ARROW_STRING, color, border); + 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); break; case DECORATION_NONE: @@ -260,8 +286,19 @@ static void enter_controls_menu(unsigned index); static void enter_joypad_menu(unsigned index); static void enter_audio_menu(unsigned index); +extern void set_filename(const char *new_filename, typeof(free) *new_free_function); +static void open_rom(unsigned index) +{ + char *filename = do_open_rom_dialog(); + if (filename) { + set_filename(filename, free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + } +} + static const struct menu_item paused_menu[] = { {"Resume", NULL}, + {"Open ROM", open_rom}, {"Emulation Options", enter_emulation_menu}, {"Graphic Options", enter_graphics_menu}, {"Audio Options", enter_audio_menu}, @@ -278,6 +315,7 @@ static void return_to_root_menu(unsigned index) { current_menu = root_menu; current_selection = 0; + scroll = 0; } static void cycle_model(unsigned index) @@ -301,10 +339,35 @@ 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"} + return (const char *[]){"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy"} [configuration.model]; } +static void cycle_sgb_revision(unsigned index) +{ + + configuration.sgb_revision++; + if (configuration.sgb_revision == SGB_MAX) { + configuration.sgb_revision = 0; + } + pending_command = GB_SDL_RESET_COMMAND; +} + +static void cycle_sgb_revision_backwards(unsigned index) +{ + if (configuration.sgb_revision == 0) { + configuration.sgb_revision = SGB_MAX; + } + configuration.sgb_revision--; + pending_command = GB_SDL_RESET_COMMAND; +} + +const char *current_sgb_revision_string(unsigned index) +{ + return (const char *[]){"Super Game Boy NTSC", "Super Game Boy PAL", "Super Game Boy 2"} + [configuration.sgb_revision]; +} + static const uint32_t rewind_lengths[] = {0, 10, 30, 60, 60 * 2, 60 * 5, 60 * 10}; static const char *rewind_strings[] = {"Disabled", "10 Seconds", @@ -353,6 +416,7 @@ const char *current_rewind_string(unsigned index) 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}, {"Rewind Length:", cycle_rewind, current_rewind_string, cycle_rewind_backwards}, {"Back", return_to_root_menu}, {NULL,} @@ -362,6 +426,7 @@ static void enter_emulation_menu(unsigned index) { current_menu = emulation_menu; current_selection = 0; + scroll = 0; } const char *current_scaling_mode(unsigned index) @@ -372,10 +437,22 @@ const char *current_scaling_mode(unsigned index) const char *current_color_correction_mode(unsigned index) { - return (const char *[]){"Disabled", "Correct Color Curves", "Emulate Hardware", "Preserve Brightness"} + return (const char *[]){"Disabled", "Correct Color Curves", "Emulate Hardware", "Preserve Brightness", "Reduce Contrast"} [configuration.color_correction_mode]; } +const char *current_palette(unsigned index) +{ + return (const char *[]){"Greyscale", "Lime (Game Boy)", "Olive (Pocket)", "Teal (Light)"} + [configuration.dmg_palette]; +} + +const char *current_border_mode(unsigned index) +{ + return (const char *[]){"SGB Only", "Never", "Always"} + [configuration.border_mode]; +} + void cycle_scaling(unsigned index) { configuration.scaling_mode++; @@ -400,7 +477,7 @@ void cycle_scaling_backwards(unsigned index) static void cycle_color_correction(unsigned index) { - if (configuration.color_correction_mode == GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS) { + if (configuration.color_correction_mode == GB_COLOR_CORRECTION_REDUCE_CONTRAST) { configuration.color_correction_mode = GB_COLOR_CORRECTION_DISABLED; } else { @@ -411,13 +488,53 @@ 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_PRESERVE_BRIGHTNESS; + configuration.color_correction_mode = GB_COLOR_CORRECTION_REDUCE_CONTRAST; } else { configuration.color_correction_mode--; } } +static void cycle_palette(unsigned index) +{ + if (configuration.dmg_palette == 3) { + configuration.dmg_palette = 0; + } + else { + configuration.dmg_palette++; + } +} + +static void cycle_palette_backwards(unsigned index) +{ + if (configuration.dmg_palette == 0) { + configuration.dmg_palette = 3; + } + else { + configuration.dmg_palette--; + } +} + +static void cycle_border_mode(unsigned index) +{ + if (configuration.border_mode == GB_BORDER_ALWAYS) { + configuration.border_mode = GB_BORDER_SGB; + } + else { + configuration.border_mode++; + } +} + +static void cycle_border_mode_backwards(unsigned index) +{ + if (configuration.border_mode == GB_BORDER_SGB) { + configuration.border_mode = GB_BORDER_ALWAYS; + } + else { + configuration.border_mode--; + } +} + struct shader_name { const char *file_name; const char *display_name; @@ -426,6 +543,7 @@ struct shader_name { {"NearestNeighbor", "Nearest Neighbor"}, {"Bilinear", "Bilinear"}, {"SmoothBilinear", "Smooth Bilinear"}, + {"MonoLCD", "Monochrome LCD"}, {"LCD", "LCD Display"}, {"CRT", "CRT Display"}, {"Scale2x", "Scale2x"}, @@ -497,21 +615,39 @@ const char *current_filter_name(unsigned index) return shaders[i].display_name; } -static void toggle_blend_frames(unsigned index) +static void cycle_blending_mode(unsigned index) { - configuration.blend_frames ^= true; + if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_ACCURATE) { + configuration.blending_mode = GB_FRAME_BLENDING_MODE_DISABLED; + } + else { + configuration.blending_mode++; + } } -const char *blend_frames_string(unsigned index) +static void cycle_blending_mode_backwards(unsigned index) { - return configuration.blend_frames? "Enabled" : "Disabled"; + if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_DISABLED) { + configuration.blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE; + } + else { + configuration.blending_mode--; + } +} + +const char *blending_mode_string(unsigned index) +{ + return (const char *[]){"Disabled", "Simple", "Accurate"} + [configuration.blending_mode]; } static const struct menu_item graphics_menu[] = { {"Scaling Mode:", cycle_scaling, current_scaling_mode, cycle_scaling_backwards}, {"Scaling Filter:", cycle_filter, current_filter_name, cycle_filter_backwards}, {"Color Correction:", cycle_color_correction, current_color_correction_mode, cycle_color_correction_backwards}, - {"Blend Frames:", toggle_blend_frames, blend_frames_string, toggle_blend_frames}, + {"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}, {"Back", return_to_root_menu}, {NULL,} }; @@ -520,6 +656,7 @@ static void enter_graphics_menu(unsigned index) { current_menu = graphics_menu; current_selection = 0; + scroll = 0; } const char *highpass_filter_string(unsigned index) @@ -546,8 +683,32 @@ void cycle_highpass_filter_backwards(unsigned index) } } +const char *volume_string(unsigned index) +{ + static char ret[5]; + sprintf(ret, "%d%%", configuration.volume); + return ret; +} + +void increase_volume(unsigned index) +{ + configuration.volume += 5; + if (configuration.volume > 100) { + configuration.volume = 100; + } +} + +void decrease_volume(unsigned index) +{ + configuration.volume -= 5; + if (configuration.volume > 100) { + configuration.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}, {"Back", return_to_root_menu}, {NULL,} }; @@ -556,6 +717,7 @@ static void enter_audio_menu(unsigned index) { current_menu = audio_menu; current_selection = 0; + scroll = 0; } static void modify_key(unsigned index) @@ -563,7 +725,6 @@ static void modify_key(unsigned index) gui_state = WAITING_FOR_KEY; } -static void enter_controls_menu_2(unsigned index); static const char *key_name(unsigned index); static const struct menu_item controls_menu[] = { @@ -575,12 +736,6 @@ static const struct menu_item controls_menu[] = { {"B:", modify_key, key_name,}, {"Select:", modify_key, key_name,}, {"Start:", modify_key, key_name,}, - {"Next Page", enter_controls_menu_2}, - {"Back", return_to_root_menu}, - {NULL,} -}; - -static const struct menu_item controls_menu_2[] = { {"Turbo:", modify_key, key_name,}, {"Rewind:", modify_key, key_name,}, {"Slow-Motion:", modify_key, key_name,}, @@ -590,11 +745,11 @@ static const struct menu_item controls_menu_2[] = { static const char *key_name(unsigned index) { - if (current_menu == controls_menu_2) { - if (index == 0) { + if (index >= 8) { + if (index == 8) { return SDL_GetScancodeName(configuration.keys[8]); } - return SDL_GetScancodeName(configuration.keys_2[index - 1]); + return SDL_GetScancodeName(configuration.keys_2[index - 9]); } return SDL_GetScancodeName(configuration.keys[index]); } @@ -603,17 +758,13 @@ static void enter_controls_menu(unsigned index) { current_menu = controls_menu; current_selection = 0; -} - -static void enter_controls_menu_2(unsigned index) -{ - current_menu = controls_menu_2; - current_selection = 0; + scroll = 0; } static unsigned joypad_index = 0; static SDL_Joystick *joystick = NULL; static SDL_GameController *controller = NULL; +SDL_Haptic *haptic = NULL; const char *current_joypad_name(unsigned index) { @@ -643,6 +794,12 @@ static void cycle_joypads(unsigned index) if (joypad_index >= SDL_NumJoysticks()) { joypad_index = 0; } + + if (haptic) { + SDL_HapticClose(haptic); + haptic = NULL; + } + if (controller) { SDL_GameControllerClose(controller); controller = NULL; @@ -651,20 +808,28 @@ static void cycle_joypads(unsigned index) SDL_JoystickClose(joystick); joystick = NULL; } - if ((controller = SDL_GameControllerOpen(joypad_index))){ + if ((controller = SDL_GameControllerOpen(joypad_index))) { joystick = SDL_GameControllerGetJoystick(controller); } else { joystick = SDL_JoystickOpen(joypad_index); } -} + if (joystick) { + haptic = SDL_HapticOpenFromJoystick(joystick); + }} static void cycle_joypads_backwards(unsigned index) { - joypad_index++; + joypad_index--; if (joypad_index >= SDL_NumJoysticks()) { joypad_index = SDL_NumJoysticks() - 1; } + + if (haptic) { + SDL_HapticClose(haptic); + haptic = NULL; + } + if (controller) { SDL_GameControllerClose(controller); controller = NULL; @@ -673,13 +838,15 @@ static void cycle_joypads_backwards(unsigned index) SDL_JoystickClose(joystick); joystick = NULL; } - if ((controller = SDL_GameControllerOpen(joypad_index))){ + if ((controller = SDL_GameControllerOpen(joypad_index))) { joystick = SDL_GameControllerGetJoystick(controller); } else { joystick = SDL_JoystickOpen(joypad_index); } -} + if (joystick) { + haptic = SDL_HapticOpenFromJoystick(joystick); + }} static void detect_joypad_layout(unsigned index) { @@ -688,9 +855,36 @@ static void detect_joypad_layout(unsigned index) joypad_axis_temp = -1; } +static void cycle_rumble_mode(unsigned index) +{ + if (configuration.rumble_mode == GB_RUMBLE_ALL_GAMES) { + configuration.rumble_mode = GB_RUMBLE_DISABLED; + } + else { + configuration.rumble_mode++; + } +} + +static void cycle_rumble_mode_backwards(unsigned index) +{ + if (configuration.rumble_mode == GB_RUMBLE_DISABLED) { + configuration.rumble_mode = GB_RUMBLE_ALL_GAMES; + } + else { + configuration.rumble_mode--; + } +} + +const char *current_rumble_mode(unsigned index) +{ + return (const char *[]){"Disabled", "Rumble Game Paks Only", "All Games"} + [configuration.rumble_mode]; +} + static const struct menu_item joypad_menu[] = { {"Joypad:", cycle_joypads, current_joypad_name, cycle_joypads_backwards}, {"Configure layout", detect_joypad_layout}, + {"Rumble Mode:", cycle_rumble_mode, current_rumble_mode, cycle_rumble_mode_backwards}, {"Back", return_to_root_menu}, {NULL,} }; @@ -699,6 +893,7 @@ static void enter_joypad_menu(unsigned index) { current_menu = joypad_menu; current_selection = 0; + scroll = 0; } joypad_button_t get_joypad_button(uint8_t physical_button) @@ -736,18 +931,21 @@ void connect_joypad(void) } } else if (!joystick && SDL_NumJoysticks()) { - if ((controller = SDL_GameControllerOpen(0))){ + if ((controller = SDL_GameControllerOpen(0))) { joystick = SDL_GameControllerGetJoystick(controller); } else { joystick = SDL_JoystickOpen(0); } } + if (joystick) { + haptic = SDL_HapticOpenFromJoystick(joystick); + } } -extern void set_filename(const char *new_filename, bool new_should_free); void run_gui(bool is_running) { + SDL_ShowCursor(SDL_ENABLE); connect_joypad(); /* Draw the background screen */ @@ -764,16 +962,84 @@ void run_gui(bool is_running) } } - uint32_t pixels[160 * 144]; + unsigned width = GB_get_screen_width(&gb); + unsigned height = GB_get_screen_height(&gb); + unsigned x_offset = (width - 160) / 2; + unsigned y_offset = (height - 144) / 2; + uint32_t pixels[width * height]; + + if (width != 160 || height != 144) { + for (unsigned i = 0; i < width * height; i++) { + pixels[i] = gui_palette_native[0]; + } + } + SDL_Event event = {0,}; gui_state = is_running? SHOWING_MENU : SHOWING_DROP_MESSAGE; bool should_render = true; current_menu = root_menu = is_running? paused_menu : nonpaused_menu; current_selection = 0; + scroll = 0; do { - /* Convert Joypad events (We only generate down events) */ + /* Convert Joypad and mouse events (We only generate down events) */ if (gui_state != WAITING_FOR_KEY && gui_state != WAITING_FOR_JBUTTON) { switch (event.type) { + case SDL_WINDOWEVENT: + should_render = true; + break; + case SDL_MOUSEBUTTONDOWN: + if (gui_state == SHOWING_HELP) { + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + } + else if (gui_state == SHOWING_DROP_MESSAGE) { + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; + } + else if (gui_state == SHOWING_MENU) { + signed x = (event.button.x - rect.x / factor) * width / (rect.w / factor) - x_offset; + signed y = (event.button.y - rect.y / factor) * height / (rect.h / factor) - y_offset; + + if (strcmp("CRT", configuration.filter) == 0) { + y = y * 8 / 7; + y -= 144 / 16; + } + y += scroll; + + if (x < 0 || x >= 160 || y < 24) { + continue; + } + + unsigned item_y = 24; + unsigned index = 0; + for (const struct menu_item *item = current_menu; item->string; item++, index++) { + if (!item->backwards_handler) { + if (y >= item_y && y < item_y + 12) { + break; + } + item_y += 12; + } + else { + if (y >= item_y && y < item_y + 24) { + break; + } + item_y += 24; + } + } + + if (!current_menu[index].string) continue; + + current_selection = index; + event.type = SDL_KEYDOWN; + if (current_menu[index].backwards_handler) { + event.key.keysym.scancode = x < 80? SDL_SCANCODE_LEFT : SDL_SCANCODE_RIGHT; + } + else { + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + } + + } + break; case SDL_JOYBUTTONDOWN: event.type = SDL_KEYDOWN; joypad_button_t button = get_joypad_button(event.jbutton.button); @@ -804,6 +1070,7 @@ void run_gui(bool is_running) event.key.keysym.scancode = scancode; } } + break; } case SDL_JOYAXISMOTION: { @@ -862,7 +1129,7 @@ void run_gui(bool is_running) break; } case SDL_DROPFILE: { - set_filename(event.drop.file, true); + set_filename(event.drop.file, SDL_free); pending_command = GB_SDL_NEW_FILE_COMMAND; return; } @@ -900,7 +1167,26 @@ void run_gui(bool is_running) } case SDL_KEYDOWN: - if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && gui_state == WAITING_FOR_JBUTTON) { + if (event.key.keysym.scancode == SDL_SCANCODE_F && event.key.keysym.mod & MODIFIER) { + if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) { + SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); + } + else { + SDL_SetWindowFullscreen(window, 0); + } + update_viewport(); + } + if (event.key.keysym.scancode == SDL_SCANCODE_O) { + if (event.key.keysym.mod & MODIFIER) { + char *filename = do_open_rom_dialog(); + if (filename) { + set_filename(filename, free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + return; + } + } + } + else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && gui_state == WAITING_FOR_JBUTTON) { should_render = true; if (joypad_configuration_progress != JOYPAD_BUTTONS_MAX) { configuration.joypad_configuration[joypad_configuration_progress] = -1; @@ -927,6 +1213,7 @@ void run_gui(bool is_running) gui_state = SHOWING_DROP_MESSAGE; } current_selection = 0; + scroll = 0; current_menu = root_menu; should_render = true; } @@ -975,12 +1262,12 @@ void run_gui(bool is_running) should_render = true; } else if (gui_state == WAITING_FOR_KEY) { - if (current_menu == controls_menu_2) { - if (current_selection == 0) { + if (current_selection >= 8) { + if (current_selection == 8) { configuration.keys[8] = event.key.keysym.scancode; } else { - configuration.keys_2[current_selection - 1] = event.key.keysym.scancode; + configuration.keys_2[current_selection - 9] = event.key.keysym.scancode; } } else { @@ -994,49 +1281,74 @@ void run_gui(bool is_running) if (should_render) { should_render = false; - memcpy(pixels, converted_background->pixels, sizeof(pixels)); + rerender: + if (width == 160 && height == 144) { + memcpy(pixels, converted_background->pixels, sizeof(pixels)); + } + else { + for (unsigned y = 0; y < 144; y++) { + memcpy(pixels + x_offset + width * (y + y_offset), ((uint32_t *)converted_background->pixels) + 160 * y, 160 * 4); + } + } switch (gui_state) { case SHOWING_DROP_MESSAGE: - draw_text_centered(pixels, 8, "Press ESC for menu", gui_palette_native[3], gui_palette_native[0], false); - draw_text_centered(pixels, 116, "Drop a GB or GBC", gui_palette_native[3], gui_palette_native[0], false); - draw_text_centered(pixels, 128, "file to play", gui_palette_native[3], gui_palette_native[0], false); + draw_text_centered(pixels, width, height, 8 + y_offset, "Press ESC for menu", gui_palette_native[3], gui_palette_native[0], false); + draw_text_centered(pixels, width, height, 116 + y_offset, "Drop a GB or GBC", gui_palette_native[3], gui_palette_native[0], false); + draw_text_centered(pixels, width, height, 128 + y_offset, "file to play", gui_palette_native[3], gui_palette_native[0], false); break; case SHOWING_MENU: - draw_text_centered(pixels, 8, "SameBoy", gui_palette_native[3], gui_palette_native[0], false); + 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 && i == 0 && scroll != 0) { + scroll = 0; + goto rerender; + } if (item->value_getter && !item->backwards_handler) { char line[25]; - snprintf(line, sizeof(line), "%s%*s", item->string, 24 - (int)strlen(item->string), item->value_getter(i)); - draw_text_centered(pixels, y, line, gui_palette_native[3], gui_palette_native[0], + snprintf(line, sizeof(line), "%s%*s", item->string, 24 - (unsigned)strlen(item->string), item->value_getter(i)); + draw_text_centered(pixels, width, height, y + y_offset, line, gui_palette_native[3], gui_palette_native[0], i == current_selection ? DECORATION_SELECTION : DECORATION_NONE); y += 12; } else { - draw_text_centered(pixels, y, item->string, gui_palette_native[3], gui_palette_native[0], + draw_text_centered(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0], i == current_selection && !item->value_getter ? DECORATION_SELECTION : DECORATION_NONE); y += 12; if (item->value_getter) { - draw_text_centered(pixels, y, item->value_getter(i), gui_palette_native[3], gui_palette_native[0], + draw_text_centered(pixels, width, height, y + y_offset, 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 (y > scroll + 144) { + scroll = y - 144; + goto rerender; + } + } + } break; case SHOWING_HELP: - draw_text(pixels, 2, 2, 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]); break; case WAITING_FOR_KEY: - draw_text_centered(pixels, 68, "Press a Key", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + draw_text_centered(pixels, width, height, 68 + y_offset, "Press a Key", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); break; case WAITING_FOR_JBUTTON: - draw_text_centered(pixels, 68, + draw_text_centered(pixels, width, height, 68 + y_offset, joypad_configuration_progress != JOYPAD_BUTTONS_MAX ? "Press button for" : "Move the Analog Stick", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); - draw_text_centered(pixels, 80, + draw_text_centered(pixels, width, height, 80 + y_offset, (const char *[]) { "Right", @@ -1054,11 +1366,15 @@ void run_gui(bool is_running) "", } [joypad_configuration_progress], gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); - draw_text_centered(pixels, 104, "Press Enter to skip", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + draw_text_centered(pixels, width, height, 104 + y_offset, "Press Enter to skip", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); break; } render_texture(pixels, NULL); +#ifdef _WIN32 + /* Required for some Windows 10 machines, god knows why */ + render_texture(pixels, NULL); +#endif } } while (SDL_WaitEvent(&event)); } diff --git a/SDL/gui.h b/SDL/gui.h index 4d10614..af7543b 100644 --- a/SDL/gui.h +++ b/SDL/gui.h @@ -1,7 +1,7 @@ #ifndef gui_h #define gui_h -#include +#include #include #include #include "shader.h" @@ -9,12 +9,19 @@ #define JOYSTICK_HIGH 0x4000 #define JOYSTICK_LOW 0x3800 +#ifdef __APPLE__ +#define MODIFIER KMOD_GUI +#else +#define MODIFIER KMOD_CTRL +#endif + extern GB_gameboy_t gb; extern SDL_Window *window; extern SDL_Renderer *renderer; extern SDL_Texture *texture; extern SDL_PixelFormat *pixel_format; +extern SDL_Haptic *haptic; extern shader_t shader; enum scaling_mode { @@ -64,7 +71,7 @@ typedef struct { SDL_Scancode keys[9]; GB_color_correction_mode_t color_correction_mode; enum scaling_mode scaling_mode; - bool blend_frames; + uint8_t blending_mode; GB_highpass_mode_t highpass_mode; @@ -77,6 +84,7 @@ typedef struct { MODEL_DMG, MODEL_CGB, MODEL_AGB, + MODEL_SGB, MODEL_MAX, } model; @@ -85,6 +93,20 @@ typedef struct { SDL_Scancode keys_2[32]; /* Rewind and underclock, + padding for the future */ uint8_t joypad_configuration[32]; /* 12 Keys + padding for the future*/; uint8_t joypad_axises[JOYPAD_AXISES_MAX]; + + /* v0.12 */ + enum { + SGB_NTSC, + SGB_PAL, + SGB_2, + SGB_MAX + } sgb_revision; + + /* v0.13 */ + uint8_t dmg_palette; + GB_border_mode_t border_mode; + uint8_t volume; + GB_rumble_mode_t rumble_mode; } configuration_t; extern configuration_t configuration; diff --git a/SDL/main.c b/SDL/main.c old mode 100755 new mode 100644 index 99facf8..3df369f --- a/SDL/main.c +++ b/SDL/main.c @@ -1,58 +1,42 @@ #include #include #include -#include +#include +#include +#include #include #include "utils.h" #include "gui.h" #include "shader.h" - +#include "audio/audio.h" #ifndef _WIN32 -#define AUDIO_FREQUENCY 96000 +#include #else -/* Windows (well, at least my VM) can't handle 96KHz sound well :( */ - -/* felsqualle says: For SDL 2.0.6+ using the WASAPI driver, the highest freq. - we can get is 48000. 96000 also works, but always has some faint crackling in - the audio, no matter how high or low I set the buffer length... - Not quite satisfied with that solution, because acc. to SDL2 docs, - 96k + WASAPI *should* work. */ - -#define AUDIO_FREQUENCY 48000 +#include #endif GB_gameboy_t gb; static bool paused = false; -static uint32_t pixel_buffer_1[160*144], pixel_buffer_2[160*144]; +static uint32_t pixel_buffer_1[256 * 224], pixel_buffer_2[256 * 224]; static uint32_t *active_pixel_buffer = pixel_buffer_1, *previous_pixel_buffer = pixel_buffer_2; static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false; static double clock_mutliplier = 1.0; static char *filename = NULL; -static bool should_free_filename = false; +static typeof(free) *free_function = NULL; static char *battery_save_path_ptr; -SDL_AudioDeviceID device_id; -static const GB_model_t sdl_to_internal_model[] = +void set_filename(const char *new_filename, typeof(free) *new_free_function) { - [MODEL_DMG] = GB_MODEL_DMG_B, - [MODEL_CGB] = GB_MODEL_CGB_E, - [MODEL_AGB] = GB_MODEL_AGB -}; - -void set_filename(const char *new_filename, bool new_should_free) -{ - if (filename && should_free_filename) { - SDL_free(filename); + if (filename && free_function) { + free_function(filename); } filename = (char *) new_filename; - should_free_filename = new_should_free; + free_function = new_free_function; } -static SDL_AudioSpec want_aspec, have_aspec; - static char *captured_log = NULL; static void log_capture_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) @@ -92,43 +76,75 @@ static const char *end_capturing_logs(bool show_popup, bool should_exit) return captured_log; } +static void update_palette(void) +{ + switch (configuration.dmg_palette) { + 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); + } +} + +static void screen_size_changed(void) +{ + SDL_DestroyTexture(texture); + texture = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_STREAMING, + GB_get_screen_width(&gb), GB_get_screen_height(&gb)); + + SDL_SetWindowMinimumSize(window, GB_get_screen_width(&gb), GB_get_screen_height(&gb)); + + update_viewport(); +} + static void open_menu(void) { - bool audio_playing = SDL_GetAudioDeviceStatus(device_id) == SDL_AUDIO_PLAYING; + bool audio_playing = GB_audio_is_playing(); if (audio_playing) { - SDL_PauseAudioDevice(device_id, 1); + GB_audio_set_paused(true); } + size_t previous_width = GB_get_screen_width(&gb); run_gui(true); + SDL_ShowCursor(SDL_DISABLE); if (audio_playing) { - SDL_PauseAudioDevice(device_id, 0); + GB_audio_set_paused(false); } GB_set_color_correction_mode(&gb, configuration.color_correction_mode); + GB_set_border_mode(&gb, configuration.border_mode); + update_palette(); GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); + if (previous_width != GB_get_screen_width(&gb)) { + screen_size_changed(); + } } static void handle_events(GB_gameboy_t *gb) { -#ifdef __APPLE__ -#define MODIFIER KMOD_GUI -#else -#define MODIFIER KMOD_CTRL -#endif 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, true); + set_filename(event.drop.file, SDL_free); pending_command = GB_SDL_NEW_FILE_COMMAND; break; } case SDL_WINDOWEVENT: { - if (event.window.event == SDL_WINDOWEVENT_RESIZED) { + if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { update_viewport(); } break; @@ -141,6 +157,7 @@ static void handle_events(GB_gameboy_t *gb) GB_set_key_state(gb, (GB_key_t) button, event.type == SDL_JOYBUTTONDOWN); } else if (button == JOYPAD_BUTTON_TURBO) { + GB_audio_clear_queue(); turbo_down = event.type == SDL_JOYBUTTONDOWN; GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); } @@ -233,13 +250,23 @@ static void handle_events(GB_gameboy_t *gb) pending_command = GB_SDL_RESET_COMMAND; } break; + + case SDL_SCANCODE_O: { + if (event.key.keysym.mod & MODIFIER) { + char *filename = do_open_rom_dialog(); + if (filename) { + set_filename(filename, free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + } + } + break; + } case SDL_SCANCODE_P: if (event.key.keysym.mod & MODIFIER) { paused = !paused; } break; - case SDL_SCANCODE_M: if (event.key.keysym.mod & MODIFIER) { #ifdef __APPLE__ @@ -248,13 +275,7 @@ static void handle_events(GB_gameboy_t *gb) break; } #endif - bool audio_playing = SDL_GetAudioDeviceStatus(device_id) == SDL_AUDIO_PLAYING; - if (audio_playing) { - SDL_PauseAudioDevice(device_id, 1); - } - else if (!audio_playing) { - SDL_PauseAudioDevice(device_id, 0); - } + GB_audio_set_paused(GB_audio_is_playing()); } break; @@ -266,6 +287,7 @@ static void handle_events(GB_gameboy_t *gb) else { SDL_SetWindowFullscreen(window, 0); } + update_viewport(); } break; @@ -288,6 +310,7 @@ static void handle_events(GB_gameboy_t *gb) case SDL_KEYUP: // Fallthrough if (event.key.keysym.scancode == configuration.keys[8]) { turbo_down = event.type == SDL_KEYDOWN; + GB_audio_clear_queue(); GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); } else if (event.key.keysym.scancode == configuration.keys_2[0]) { @@ -317,14 +340,14 @@ static void handle_events(GB_gameboy_t *gb) static void vblank(GB_gameboy_t *gb) { if (underclock_down && clock_mutliplier > 0.5) { - clock_mutliplier -= 0.1; + clock_mutliplier -= 1.0/16; GB_set_clock_multiplier(gb, clock_mutliplier); } else if (!underclock_down && clock_mutliplier < 1.0) { - clock_mutliplier += 0.1; + clock_mutliplier += 1.0/16; GB_set_clock_multiplier(gb, clock_mutliplier); } - if (configuration.blend_frames) { + if (configuration.blending_mode) { render_texture(active_pixel_buffer, previous_pixel_buffer); uint32_t *temp = active_pixel_buffer; active_pixel_buffer = previous_pixel_buffer; @@ -344,6 +367,11 @@ 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); +} + static void debugger_interrupt(int ignore) { if (!GB_is_inited(&gb)) return; @@ -355,16 +383,32 @@ static void debugger_interrupt(int ignore) GB_debugger_break(&gb); } - -static void audio_callback(void *gb, Uint8 *stream, int len) -{ - if (GB_is_inited(gb)) { - GB_apu_copy_buffer(gb, (GB_sample_t *) stream, len / sizeof(GB_sample_t)); +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) { + skip = 0; + } + if (skip > GB_audio_get_frequency() / 16) { + return; + } } - else { - memset(stream, 0, len); + + if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_frequency() / 4) { + return; } + + if (configuration.volume != 100) { + sample->left = sample->left * configuration.volume / 100; + sample->right = sample->right * configuration.volume / 100; + } + + GB_audio_queue_sample(sample); + } + static bool handle_pending_command(void) { @@ -387,14 +431,12 @@ static bool handle_pending_command(void) return false; } - case GB_SDL_RESET_COMMAND: - GB_save_battery(&gb, battery_save_path_ptr); - return true; - case GB_SDL_NO_COMMAND: return false; + case GB_SDL_RESET_COMMAND: case GB_SDL_NEW_FILE_COMMAND: + GB_save_battery(&gb, battery_save_path_ptr); return true; case GB_SDL_QUIT_COMMAND: @@ -404,36 +446,92 @@ static bool handle_pending_command(void) return false; } +static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) +{ + bool error = false; + start_capturing_logs(); + static const char *const names[] = { + [GB_BOOT_ROM_DMG0] = "dmg0_boot.bin", + [GB_BOOT_ROM_DMG] = "dmg_boot.bin", + [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] = "cgb_boot.bin", + [GB_BOOT_ROM_AGB] = "agb_boot.bin", + }; + GB_load_boot_rom(gb, resource_path(names[type])); + end_capturing_logs(true, error); +} + static void run(void) { + SDL_ShowCursor(SDL_DISABLE); + GB_model_t model; pending_command = GB_SDL_NO_COMMAND; restart: + model = (GB_model_t []) + { + [MODEL_DMG] = GB_MODEL_DMG_B, + [MODEL_CGB] = GB_MODEL_CGB_E, + [MODEL_AGB] = GB_MODEL_AGB, + [MODEL_SGB] = (GB_model_t []) + { + [SGB_NTSC] = GB_MODEL_SGB_NTSC, + [SGB_PAL] = GB_MODEL_SGB_PAL, + [SGB_2] = GB_MODEL_SGB2, + }[configuration.sgb_revision], + }[configuration.model]; + if (GB_is_inited(&gb)) { - GB_switch_model_and_reset(&gb, sdl_to_internal_model[configuration.model]); + GB_switch_model_and_reset(&gb, model); } else { - GB_init(&gb, sdl_to_internal_model[configuration.model]); + GB_init(&gb, model); + GB_set_boot_rom_load_callback(&gb, load_boot_rom); GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); GB_set_pixels_output(&gb, active_pixel_buffer); GB_set_rgb_encode_callback(&gb, rgb_encode); - GB_set_sample_rate(&gb, have_aspec.freq); + 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_color_correction_mode(&gb, configuration.color_correction_mode); + 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_update_input_hint_callback(&gb, handle_events); + GB_apu_set_sample_callback(&gb, gb_audio_callback); } - + bool error = false; + GB_debugger_clear_symbols(&gb); start_capturing_logs(); - const char * const boot_roms[] = {"dmg_boot.bin", "cgb_boot.bin", "agb_boot.bin"}; - error = GB_load_boot_rom(&gb, resource_path(boot_roms[configuration.model])); - end_capturing_logs(true, error); - - start_capturing_logs(); - error = GB_load_rom(&gb, filename); - end_capturing_logs(true, error); - size_t path_length = strlen(filename); + 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]); + } + } + if (strcmp(extension, "isx") == 0) { + error = GB_load_isx(&gb, filename); + /* Configure battery */ + char battery_save_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ + replace_extension(filename, path_length, battery_save_path, ".ram"); + battery_save_path_ptr = battery_save_path; + GB_load_battery(&gb, battery_save_path); + } + else { + GB_load_rom(&gb, filename); + } + end_capturing_logs(true, error); + /* Configure battery */ char battery_save_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ @@ -447,7 +545,9 @@ restart: char symbols_path[path_length + 5]; replace_extension(filename, path_length, symbols_path, ".sym"); GB_debugger_load_symbol_file(&gb, symbols_path); - + + screen_size_changed(); + /* Run emulation */ while (true) { if (paused || rewind_paused) { @@ -502,6 +602,9 @@ static bool get_arg_flag(const char *flag, int *argc, char **argv) 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"); @@ -553,55 +656,34 @@ int main(int argc, char **argv) pixel_format = SDL_AllocFormat(SDL_PIXELFORMAT_ABGR8888); } - - /* Configure Audio */ - memset(&want_aspec, 0, sizeof(want_aspec)); - want_aspec.freq = AUDIO_FREQUENCY; - want_aspec.format = AUDIO_S16SYS; - want_aspec.channels = 2; - want_aspec.samples = 512; - - SDL_version _sdl_version; - SDL_GetVersion(&_sdl_version); - unsigned sdl_version = _sdl_version.major * 1000 + _sdl_version.minor * 100 + _sdl_version.patch; - -#ifndef _WIN32 - /* SDL 2.0.5 on macOS and Linux introduced a bug where certain combinations of buffer lengths and frequencies - fail to produce audio correctly. */ - if (sdl_version >= 2005) { - want_aspec.samples = 2048; - } -#else - if (sdl_version >= 2006) { - /* SDL 2.0.6 offers WASAPI support which allows for much lower audio buffer lengths which at least - theoretically reduces lagging. */ - want_aspec.samples = 32; - } - else { - /* Since WASAPI audio was introduced in SDL 2.0.6, we have to lower the audio frequency - to 44100 because otherwise we would get garbled audio output.*/ - want_aspec.freq = 44100; - } -#endif - - - want_aspec.callback = audio_callback; - want_aspec.userdata = &gb; - device_id = SDL_OpenAudioDevice(0, 0, &want_aspec, &have_aspec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); - - /* Start Audio */ + GB_audio_init(); SDL_EventState(SDL_DROPFILE, SDL_ENABLE); - char *prefs_dir = SDL_GetPrefPath("", "SameBoy"); - snprintf(prefs_path, sizeof(prefs_path) - 1, "%sprefs.bin", prefs_dir); - SDL_free(prefs_dir); + strcpy(prefs_path, resource_path("prefs.bin")); + if (access(prefs_path, R_OK | W_OK) != 0) { + char *prefs_dir = SDL_GetPrefPath("", "SameBoy"); + snprintf(prefs_path, sizeof(prefs_path) - 1, "%sprefs.bin", prefs_dir); + SDL_free(prefs_dir); + } FILE *prefs_file = fopen(prefs_path, "rb"); if (prefs_file) { fread(&configuration, 1, sizeof(configuration), prefs_file); fclose(prefs_file); + + /* Sanitize for stability */ + configuration.color_correction_mode %= GB_COLOR_CORRECTION_REDUCE_CONTRAST +1; + configuration.scaling_mode %= GB_SDL_SCALING_MAX; + configuration.blending_mode %= GB_FRAME_BLENDING_MODE_ACCURATE + 1; + configuration.highpass_mode %= GB_HIGHPASS_MAX; + configuration.model %= MODEL_MAX; + configuration.sgb_revision %= SGB_MAX; + configuration.dmg_palette %= 3; + configuration.border_mode %= GB_BORDER_ALWAYS + 1; + configuration.rumble_mode %= GB_RUMBLE_ALL_GAMES + 1; } + if (configuration.model >= MODEL_MAX) { configuration.model = MODEL_CGB; } @@ -619,7 +701,7 @@ int main(int argc, char **argv) else { connect_joypad(); } - SDL_PauseAudioDevice(device_id, 0); + GB_audio_set_paused(false); run(); // Never returns return 0; } diff --git a/SDL/opengl_compat.c b/SDL/opengl_compat.c index aed2a76..af7ce6d 100644 --- a/SDL/opengl_compat.c +++ b/SDL/opengl_compat.c @@ -1,5 +1,5 @@ #define GL_GLEXT_PROTOTYPES -#include +#include #ifndef __APPLE__ #define GL_COMPAT_NAME(func) gl_compat_##func diff --git a/SDL/opengl_compat.h b/SDL/opengl_compat.h index 9fd1ca9..4b79b0c 100644 --- a/SDL/opengl_compat.h +++ b/SDL/opengl_compat.h @@ -2,15 +2,15 @@ #define opengl_compat_h #define GL_GLEXT_PROTOTYPES -#include -#include +#include +#include #ifndef __APPLE__ #define GL_COMPAT_NAME(func) gl_compat_##func #define GL_COMPAT_WRAPPER(func) \ ({ extern typeof(func) *GL_COMPAT_NAME(func); \ -if(!GL_COMPAT_NAME(func)) GL_COMPAT_NAME(func) = SDL_GL_GetProcAddress(#func); \ +if (!GL_COMPAT_NAME(func)) GL_COMPAT_NAME(func) = SDL_GL_GetProcAddress(#func); \ GL_COMPAT_NAME(func); \ }) diff --git a/SDL/shader.c b/SDL/shader.c index ed45c42..de2ba56 100644 --- a/SDL/shader.c +++ b/SDL/shader.c @@ -75,7 +75,7 @@ bool init_shader_with_name(shader_t *shader, const char *name) static char master_shader_code[0x801] = {0,}; static char shader_code[0x10001] = {0,}; static char final_shader_code[0x10801] = {0,}; - static signed long filter_token_location = 0; + static ssize_t filter_token_location = 0; if (!master_shader_code[0]) { FILE *master_shader_f = fopen(resource_path("Shaders/MasterShader.fsh"), "r"); @@ -130,7 +130,7 @@ bool init_shader_with_name(shader_t *shader, const char *name) glBindTexture(GL_TEXTURE_2D, 0); shader->previous_texture_uniform = glGetUniformLocation(shader->program, "previous_image"); - shader->mix_previous_uniform = glGetUniformLocation(shader->program, "mix_previous"); + shader->blending_mode_uniform = glGetUniformLocation(shader->program, "frame_blending_mode"); // Program @@ -162,20 +162,23 @@ bool init_shader_with_name(shader_t *shader, const char *name) return true; } -void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous,unsigned x, unsigned y, unsigned w, unsigned h) +void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous, + unsigned source_width, unsigned source_height, + unsigned x, unsigned y, unsigned w, unsigned h, + GB_frame_blending_mode_t blending_mode) { glUseProgram(shader->program); glUniform2f(shader->origin_uniform, x, y); glUniform2f(shader->resolution_uniform, w, h); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, shader->texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 160, 144, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitmap); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, source_width, source_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitmap); glUniform1i(shader->texture_uniform, 0); - glUniform1i(shader->mix_previous_uniform, previous != NULL); + glUniform1i(shader->blending_mode_uniform, previous? blending_mode : GB_FRAME_BLENDING_MODE_DISABLED); if (previous) { glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, shader->previous_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 160, 144, 0, GL_RGBA, GL_UNSIGNED_BYTE, previous); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, source_width, source_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, previous); glUniform1i(shader->previous_texture_uniform, 1); } glBindFragDataLocation(shader->program, 0, "frag_color"); diff --git a/SDL/shader.h b/SDL/shader.h index 20baf76..149958d 100644 --- a/SDL/shader.h +++ b/SDL/shader.h @@ -8,7 +8,7 @@ typedef struct shader_s { GLuint origin_uniform; GLuint texture_uniform; GLuint previous_texture_uniform; - GLuint mix_previous_uniform; + GLuint blending_mode_uniform; GLuint position_attribute; GLuint texture; @@ -16,8 +16,19 @@ typedef struct shader_s { GLuint program; } shader_t; +typedef enum { + GB_FRAME_BLENDING_MODE_DISABLED, + GB_FRAME_BLENDING_MODE_SIMPLE, + GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_EVEN = GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_ODD, +} GB_frame_blending_mode_t; + bool init_shader_with_name(shader_t *shader, const char *name); -void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous,unsigned x, unsigned y, unsigned w, unsigned h); +void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous, + unsigned source_width, unsigned source_height, + unsigned x, unsigned y, unsigned w, unsigned h, + GB_frame_blending_mode_t blending_mode); void free_shader(struct shader_s *shader); #endif /* shader_h */ diff --git a/SDL/utils.c b/SDL/utils.c index eee6ce6..8cdd00b 100644 --- a/SDL/utils.c +++ b/SDL/utils.c @@ -1,4 +1,4 @@ -#include +#include #include #include #include "utils.h" diff --git a/Shaders/CRT.fsh b/Shaders/CRT.fsh index cbb1528..0bc4c65 100644 --- a/Shaders/CRT.fsh +++ b/Shaders/CRT.fsh @@ -55,7 +55,7 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou right *= scanline_multiplier; /* Vertical seperator for shadow masks */ - bool odd = (int)(position * input_resolution).x & 1; + bool odd = bool(int((position * input_resolution).x) & 1); if (odd) { pos.y += 0.5; pos.y = fract(pos.y); @@ -158,6 +158,8 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou ret *= output_resolution.y - pixel_position.y; } + // Gamma correction + ret = pow(ret, vec4(0.72)); return ret; } diff --git a/Shaders/HQ2x.fsh b/Shaders/HQ2x.fsh index 3871db9..2e19fa6 100644 --- a/Shaders/HQ2x.fsh +++ b/Shaders/HQ2x.fsh @@ -62,40 +62,54 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou if (is_different(w7, w4)) pattern |= 64; if (is_different(w8, w4)) pattern |= 128; - if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) + if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) { return interp_2px(w4, 3.0, w3, 1.0); - if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) + } + if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) { return interp_2px(w4, 3.0, w1, 1.0); - if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) + } + if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) { return w4; + } if ((P(0x6f,0x2a) || P(0x5b,0x0a) || P(0xbf,0x3a) || P(0xdf,0x5a) || P(0x9f,0x8a) || P(0xcf,0x8a) || P(0xef,0x4e) || P(0x3f,0x0e) || P(0xfb,0x5a) || P(0xbb,0x8a) || P(0x7f,0x5a) || P(0xaf,0x8a) || - P(0xeb,0x8a)) && is_different(w3, w1)) + P(0xeb,0x8a)) && is_different(w3, w1)) { return interp_2px(w4, 3.0, w0, 1.0); - if (P(0x0b,0x08)) + } + if (P(0x0b,0x08)) { return interp_3px(w4, 2.0, w0, 1.0, w1, 1.0); - if (P(0x0b,0x02)) + } + if (P(0x0b,0x02)) { return interp_3px(w4, 2.0, w0, 1.0, w3, 1.0); - if (P(0x2f,0x2f)) + } + if (P(0x2f,0x2f)) { return interp_3px(w4, 1.04, w3, 1.0, w1, 1.0); - if (P(0xbf,0x37) || P(0xdb,0x13)) + } + if (P(0xbf,0x37) || P(0xdb,0x13)) { return interp_3px(w4, 5.0, w1, 2.0, w3, 1.0); - if (P(0xdb,0x49) || P(0xef,0x6d)) + } + if (P(0xdb,0x49) || P(0xef,0x6d)) { return interp_3px(w4, 5.0, w3, 2.0, w1, 1.0); - if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) + } + if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) { return interp_2px(w4, 3.0, w3, 1.0); - if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) + } + if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) { return interp_2px(w4, 3.0, w1, 1.0); - if (P(0x7e,0x2a) || P(0xef,0xab) || P(0xbf,0x8f) || P(0x7e,0x0e)) + } + if (P(0x7e,0x2a) || P(0xef,0xab) || P(0xbf,0x8f) || P(0x7e,0x0e)) { return interp_3px(w4, 2.0, w3, 3.0, w1, 3.0); + } if (P(0xfb,0x6a) || P(0x6f,0x6e) || P(0x3f,0x3e) || P(0xfb,0xfa) || - P(0xdf,0xde) || P(0xdf,0x1e)) + P(0xdf,0xde) || P(0xdf,0x1e)) { return interp_2px(w4, 3.0, w0, 1.0); + } if (P(0x0a,0x00) || P(0x4f,0x4b) || P(0x9f,0x1b) || P(0x2f,0x0b) || P(0xbe,0x0a) || P(0xee,0x0a) || P(0x7e,0x0a) || P(0xeb,0x4b) || - P(0x3b,0x1b)) + P(0x3b,0x1b)) { return interp_3px(w4, 2.0, w3, 1.0, w1, 1.0); + } return interp_3px(w4, 6.0, w3, 1.0, w1, 1.0); } diff --git a/Shaders/MasterShader.fsh b/Shaders/MasterShader.fsh index a489cf7..729ab5f 100644 --- a/Shaders/MasterShader.fsh +++ b/Shaders/MasterShader.fsh @@ -1,11 +1,10 @@ #version 150 uniform sampler2D image; uniform sampler2D previous_image; -uniform bool mix_previous; +uniform int frame_blending_mode; uniform vec2 output_resolution; uniform vec2 origin; -const vec2 input_resolution = vec2(160, 144); #define equal(x, y) ((x) == (y)) #define inequal(x, y) ((x) != (y)) @@ -16,6 +15,15 @@ out vec4 frag_color; #line 1 {filter} + +#define BLEND_BIAS (1.0/3.0) + +#define DISABLED 0 +#define SIMPLE 1 +#define ACCURATE 2 +#define ACCURATE_EVEN ACCURATE +#define ACCURATE_ODD 3 + void main() { vec2 position = gl_FragCoord.xy - origin; @@ -23,11 +31,34 @@ void main() position.y = 1 - position.y; vec2 input_resolution = textureSize(image, 0); - if (mix_previous) { - frag_color = mix(scale(image, position, input_resolution, output_resolution), - scale(previous_image, position, input_resolution, output_resolution), 0.5); - } - else { - frag_color = scale(image, position, input_resolution, output_resolution); + float ratio; + switch (frame_blending_mode) { + default: + case DISABLED: + frag_color = scale(image, position, input_resolution, output_resolution); + return; + case SIMPLE: + ratio = 0.5; + break; + case ACCURATE_EVEN: + if ((int(position.y * input_resolution.y) & 1) == 0) { + ratio = BLEND_BIAS; + } + else { + ratio = 1 - BLEND_BIAS; + } + break; + case ACCURATE_ODD: + if ((int(position.y * input_resolution.y) & 1) == 0) { + ratio = 1 - BLEND_BIAS; + } + else { + ratio = BLEND_BIAS; + } + break; } + + frag_color = mix(scale(image, position, input_resolution, output_resolution), + scale(previous_image, position, input_resolution, output_resolution), ratio); + } diff --git a/Shaders/MasterShader.metal b/Shaders/MasterShader.metal index 4cae3ae..a0b6393 100644 --- a/Shaders/MasterShader.metal +++ b/Shaders/MasterShader.metal @@ -42,19 +42,52 @@ static inline float4 texture(texture2d texture, float2 pos) #line 1 {filter} +#define BLEND_BIAS (2.0/5.0) + +enum frame_blending_mode { + DISABLED, + SIMPLE, + ACCURATE, + ACCURATE_EVEN = ACCURATE, + ACCURATE_ODD, +}; + fragment float4 fragment_shader(rasterizer_data in [[stage_in]], texture2d image [[ texture(0) ]], texture2d previous_image [[ texture(1) ]], - constant bool *mix_previous [[ buffer(0) ]], + constant enum frame_blending_mode *frame_blending_mode [[ buffer(0) ]], constant float2 *output_resolution [[ buffer(1) ]]) { float2 input_resolution = float2(image.get_width(), image.get_height()); in.texcoords.y = 1 - in.texcoords.y; - if (*mix_previous) { - return mix(scale(image, in.texcoords, input_resolution, *output_resolution), - scale(previous_image, in.texcoords, input_resolution, *output_resolution), 0.5); + float ratio; + switch (*frame_blending_mode) { + default: + case DISABLED: + return scale(image, in.texcoords, input_resolution, *output_resolution); + case SIMPLE: + ratio = 0.5; + break; + case ACCURATE_EVEN: + if (((int)(in.texcoords.y * input_resolution.y) & 1) == 0) { + ratio = BLEND_BIAS; + } + else { + ratio = 1 - BLEND_BIAS; + } + break; + case ACCURATE_ODD: + if (((int)(in.texcoords.y * input_resolution.y) & 1) == 0) { + ratio = 1 - BLEND_BIAS; + } + else { + ratio = BLEND_BIAS; + } + break; } - return scale(image, in.texcoords, input_resolution, *output_resolution); + + return mix(scale(image, in.texcoords, input_resolution, *output_resolution), + scale(previous_image, in.texcoords, input_resolution, *output_resolution), ratio); } diff --git a/Shaders/MonoLCD.fsh b/Shaders/MonoLCD.fsh new file mode 100644 index 0000000..009e1db --- /dev/null +++ b/Shaders/MonoLCD.fsh @@ -0,0 +1,50 @@ +#define SCANLINE_DEPTH 0.25 +#define BLOOM 0.4 + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pixel = position * input_resolution - vec2(0.5, 0.5); + + vec4 q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + vec4 q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + vec4 q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + vec4 q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + vec2 s = smoothstep(0., 1., fract(pixel)); + + vec4 r1 = mix(q11, q21, s.x); + vec4 r2 = mix(q12, q22, s.x); + + vec2 pos = fract(position * input_resolution); + vec2 sub_pos = fract(position * input_resolution * 6); + + float multiplier = 1.0; + + if (pos.y < 1.0 / 6.0) { + multiplier *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + else if (pos.y > 5.0 / 6.0) { + multiplier *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + + if (pos.x < 1.0 / 6.0) { + multiplier *= sub_pos.x * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + else if (pos.x > 5.0 / 6.0) { + multiplier *= (1.0 - sub_pos.x) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + + vec4 pre_shadow = mix(texture(image, position) * multiplier, mix(r1, r2, s.y), BLOOM); + pixel += vec2(-0.6, -0.8); + + q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + r1 = mix(q11, q21, fract(pixel.x)); + r2 = mix(q12, q22, fract(pixel.x)); + + vec4 shadow = mix(r1, r2, fract(pixel.y)); + return mix(min(shadow, pre_shadow), pre_shadow, 0.75); +} diff --git a/Shaders/OmniScale.fsh b/Shaders/OmniScale.fsh index bb2b7d6..c76f736 100644 --- a/Shaders/OmniScale.fsh +++ b/Shaders/OmniScale.fsh @@ -63,21 +63,27 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou if (is_different(w7, w4)) pattern |= 1 << 6; if (is_different(w8, w4)) pattern |= 1 << 7; - if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) + if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) { return mix(w4, w3, 0.5 - p.x); - if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) + } + if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) { return mix(w4, w1, 0.5 - p.y); - if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) + } + if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) { return w4; + } if ((P(0x6f,0x2a) || P(0x5b,0x0a) || P(0xbf,0x3a) || P(0xdf,0x5a) || P(0x9f,0x8a) || P(0xcf,0x8a) || P(0xef,0x4e) || P(0x3f,0x0e) || P(0xfb,0x5a) || P(0xbb,0x8a) || P(0x7f,0x5a) || P(0xaf,0x8a) || - P(0xeb,0x8a)) && is_different(w3, w1)) + P(0xeb,0x8a)) && is_different(w3, w1)) { return mix(w4, mix(w4, w0, 0.5 - p.x), 0.5 - p.y); - if (P(0x0b,0x08)) + } + if (P(0x0b,0x08)) { return mix(mix(w0 * 0.375 + w1 * 0.25 + w4 * 0.375, w4 * 0.5 + w1 * 0.5, p.x * 2.0), w4, p.y * 2.0); - if (P(0x0b,0x02)) + } + if (P(0x0b,0x02)) { return mix(mix(w0 * 0.375 + w3 * 0.25 + w4 * 0.375, w4 * 0.5 + w3 * 0.5, p.y * 2.0), w4, p.x * 2.0); + } if (P(0x2f,0x2f)) { float dist = length(p - vec2(0.5)); float pixel_size = length(1.0 / (output_resolution / input_resolution)); @@ -142,7 +148,6 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou } return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); - } if (P(0x7e,0x2a) || P(0xef,0xab)) { @@ -169,15 +174,18 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); } - if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) + if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) { return mix(w4, w3, 0.5 - p.x); + } - if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) + if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) { return mix(w4, w1, 0.5 - p.y); + } if (P(0xfb,0x6a) || P(0x6f,0x6e) || P(0x3f,0x3e) || P(0xfb,0xfa) || - P(0xdf,0xde) || P(0xdf,0x1e)) + P(0xdf,0xde) || P(0xdf,0x1e)) { return mix(w4, w0, (1.0 - p.x - p.y) / 2.0); + } if (P(0x4f,0x4b) || P(0x9f,0x1b) || P(0x2f,0x0b) || P(0xbe,0x0a) || P(0xee,0x0a) || P(0x7e,0x0a) || P(0xeb,0x4b) || @@ -204,17 +212,20 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou return mix(r, w4, (dist + pixel_size / 2 - 0.5) / pixel_size); } - if (P(0x0b,0x01)) + if (P(0x0b,0x01)) { return mix(mix(w4, w3, 0.5 - p.x), mix(w1, (w1 + w3) / 2.0, 0.5 - p.x), 0.5 - p.y); + } - if (P(0x0b,0x00)) + if (P(0x0b,0x00)) { return mix(mix(w4, w3, 0.5 - p.x), mix(w1, w0, 0.5 - p.x), 0.5 - p.y); + } float dist = p.x + p.y; float pixel_size = length(1.0 / (output_resolution / input_resolution)); - if (dist > 0.5 + pixel_size / 2) + if (dist > 0.5 + pixel_size / 2) { return w4; + } /* We need more samples to "solve" this diagonal */ vec4 x0 = texture(image, position + vec2( -o.x * 2.0, -o.y * 2.0)); @@ -239,7 +250,7 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou pattern >>= 1; } - if (diagonal_bias <= 0) { + if (diagonal_bias <= 0) { vec4 r = mix(w1, w3, p.y - p.x + 0.5); if (dist < 0.5 - pixel_size / 2) { return r; diff --git a/Tester/main.c b/Tester/main.c index 643c39f..16dbf7b 100755 --- a/Tester/main.c +++ b/Tester/main.c @@ -16,12 +16,7 @@ #endif #include - -/* Disable all randomness during automatic tests */ -long random(void) -{ - return 0; -} +#include static bool running = false; static char *filename; @@ -30,7 +25,8 @@ static char *log_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; + do_not_stop, push_a_twice, start_is_bad, allow_weird_sp_values, large_stack, push_right, + semi_random, limit_start, pointer_control; static unsigned int test_length = 60 * 40; GB_gameboy_t gb; @@ -54,47 +50,70 @@ static char *async_input_callback(GB_gameboy_t *gb) return NULL; } -static void vblank(GB_gameboy_t *gb) +static void handle_buttons(GB_gameboy_t *gb) { /* 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. */ - if (push_start_a && (frames < test_length - 120 || do_not_stop)) { + screenshot to be taken while the LCD is off if the press makes the game + load graphics. */ + if (push_start_a && (frames < test_length - 120 || do_not_stop)) { unsigned combo_length = 40; if (start_is_not_first || push_a_twice) combo_length = 60; /* The start item in the menu is not the first, so also push down */ else if (a_is_bad || start_is_bad) combo_length = 20; /* Pressing A has a negative effect (when trying to start the game). */ - - switch ((push_faster ? frames * 2 : - push_slower ? frames / 2 : - push_a_twice? frames / 4: - frames) % combo_length + (start_is_bad? 20 : 0) ) { - case 0: - gb->keys[0][push_right? 0 : 7] = true; // Start (Or right) down - break; - case 10: - gb->keys[0][push_right? 0 : 7] = false; // Start (Or right) up - break; - case 20: - gb->keys[0][b_is_confirm? 5: 4] = true; // A down (or B) - break; - case 30: - gb->keys[0][b_is_confirm? 5: 4] = false; // A up (or B) - break; - case 40: - if (push_a_twice) { - gb->keys[0][b_is_confirm? 5: 4] = true; // A down (or B) - } - else if (gb->boot_rom_finished) { - gb->keys[0][3] = true; // D-Pad Down down - } - break; - case 50: - gb->keys[0][b_is_confirm? 5: 4] = false; // A down (or B) - gb->keys[0][3] = false; // D-Pad Down up - break; + + if (semi_random) { + if (frames % 10 == 0) { + unsigned key = (((frames / 20) * 0x1337cafe) >> 29) & 7; + gb->keys[0][key] = (frames % 20) == 0; + } + } + else { + switch ((push_faster ? frames * 2 : + push_slower ? frames / 2 : + push_a_twice? frames / 4: + frames) % combo_length + (start_is_bad? 20 : 0) ) { + case 0: + if (!limit_start || frames < 20 * 60) { + GB_set_key_state(gb, push_right? GB_KEY_RIGHT: GB_KEY_START, true); + } + if (pointer_control) { + GB_set_key_state(gb, GB_KEY_LEFT, true); + GB_set_key_state(gb, GB_KEY_UP, true); + } + + break; + case 10: + GB_set_key_state(gb, push_right? GB_KEY_RIGHT: GB_KEY_START, false); + if (pointer_control) { + GB_set_key_state(gb, GB_KEY_LEFT, false); + GB_set_key_state(gb, GB_KEY_UP, false); + } + break; + case 20: + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, true); + break; + case 30: + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, false); + break; + case 40: + if (push_a_twice) { + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, true); + } + else if (gb->boot_rom_finished) { + GB_set_key_state(gb, GB_KEY_DOWN, true); + } + break; + case 50: + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, false); + GB_set_key_state(gb, GB_KEY_DOWN, false); + break; + } } } - + +} + +static void vblank(GB_gameboy_t *gb) +{ /* Detect common crashes and stop the test early */ if (frames < test_length - 1) { if (gb->backtrace_size >= 0x200 + (large_stack? 0x80: 0) || (!allow_weird_sp_values && (gb->registers[GB_REGISTER_SP] >= 0xfe00 && gb->registers[GB_REGISTER_SP] < 0xff80))) { @@ -108,7 +127,7 @@ static void vblank(GB_gameboy_t *gb) } } - if (frames >= test_length ) { + if (frames >= test_length && !gb->disable_rendering) { bool is_screen_blank = true; for (unsigned i = 160*144; i--;) { if (bitmap[i] != bitmap[0]) { @@ -132,11 +151,9 @@ static void vblank(GB_gameboy_t *gb) running = false; } } - else if (frames == test_length - 1) { + else if (frames >= test_length - 1) { gb->disable_rendering = false; } - - frames++; } static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) @@ -157,12 +174,12 @@ static const char *executable_folder(void) } /* Ugly unportable code! :( */ #ifdef __APPLE__ - unsigned int length = sizeof(path) - 1; + uint32_t length = sizeof(path) - 1; _NSGetExecutablePath(&path[0], &length); #else #ifdef __linux__ - ssize_t length = readlink("/proc/self/exe", &path[0], sizeof(path) - 1); - assert (length != -1); + size_t __attribute__((unused)) length = readlink("/proc/self/exe", &path[0], sizeof(path) - 1); + assert(length != -1); #else #ifdef _WIN32 HMODULE hModule = GetModuleHandle(NULL); @@ -242,6 +259,8 @@ int main(int argc, char **argv) bool dmg = false; const char *boot_rom_path = NULL; + + GB_random_set_enabled(false); for (unsigned i = 1; i < argc; i++) { if (strcmp(argv[i], "--dmg") == 0) { @@ -281,7 +300,7 @@ int main(int argc, char **argv) if (max_forks > 1) { while (current_forks >= max_forks) { int wait_out; - while(wait(&wait_out) == -1); + while (wait(&wait_out) == -1); current_forks--; } @@ -304,15 +323,15 @@ int main(int argc, char **argv) if (dmg) { GB_init(&gb, GB_MODEL_DMG_B); - if (GB_load_boot_rom(&gb, boot_rom_path? boot_rom_path : executable_relative_path("dmg_boot.bin"))) { - perror("Failed to load boot ROM"); + if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("dmg_boot.bin"))) { + fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("dmg_boot.bin")); exit(1); } } else { GB_init(&gb, GB_MODEL_CGB_E); - if (GB_load_boot_rom(&gb, boot_rom_path? boot_rom_path : executable_relative_path("cgb_boot.bin"))) { - perror("Failed to load boot ROM"); + if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("cgb_boot.bin"))) { + fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("cgb_boot.bin")); exit(1); } } @@ -322,6 +341,7 @@ int main(int argc, char **argv) GB_set_rgb_encode_callback(&gb, rgb_encode); 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); if (GB_load_rom(&gb, filename)) { perror("Failed to load ROM"); @@ -336,9 +356,14 @@ int main(int argc, char **argv) /* Restarting in Puzzle Boy/Kwirk (Start followed by A) leaks stack. */ strcmp((const char *)(gb.rom + 0x134), "KWIRK") == 0 || strcmp((const char *)(gb.rom + 0x134), "PUZZLE BOY") == 0; - start_is_bad = strcmp((const char *)(gb.rom + 0x134), "BLUESALPHA") == 0; - b_is_confirm = strcmp((const char *)(gb.rom + 0x134), "ELITE SOCCER") == 0; - push_faster = strcmp((const char *)(gb.rom + 0x134), "MOGURA DE PON!") == 0; + start_is_bad = strcmp((const char *)(gb.rom + 0x134), "BLUESALPHA") == 0 || + 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; + 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; push_slower = strcmp((const char *)(gb.rom + 0x134), "BAKENOU") == 0; do_not_stop = strcmp((const char *)(gb.rom + 0x134), "SPACE INVADERS") == 0; push_right = memcmp((const char *)(gb.rom + 0x134), "BOB ET BOB", strlen("BOB ET BOB")) == 0 || @@ -346,6 +371,9 @@ int main(int argc, char **argv) /* M&M's Minis Madness Demo (which has no menu but the same title as the full game) */ (memcmp((const char *)(gb.rom + 0x134), "MINIMADNESSBMIE", strlen("MINIMADNESSBMIE")) == 0 && gb.rom[0x14e] == 0x6c); + /* This game has some terrible menus. */ + semi_random = strcmp((const char *)(gb.rom + 0x134), "KUKU GAME") == 0; + /* This game temporarily sets SP to OAM RAM */ @@ -356,6 +384,10 @@ int main(int argc, char **argv) /* This game uses some recursive algorithms and therefore requires quite a large call stack */ large_stack = memcmp((const char *)(gb.rom + 0x134), "MICRO EPAK1BM", strlen("MICRO EPAK1BM")) == 0 || strcmp((const char *)(gb.rom + 0x134), "TECMO BOWL") == 0; + /* High quality game that leaks stack whenever you open the menu (with start), + but requires pressing start to play it. */ + limit_start = strcmp((const char *)(gb.rom + 0x134), "DIVA STARS") == 0; + large_stack |= limit_start; /* Pressing start while in the map in Tsuri Sensei will leak an internal screen-stack which will eventually overflow, override an array of jump-table indexes, jump to a random @@ -363,12 +395,22 @@ int main(int argc, char **argv) will prevent this scenario. */ push_a_twice = strcmp((const char *)(gb.rom + 0x134), "TURI SENSEI V1") == 0; + /* Yes, you should totally use a cursor point & click interface for the language select menu. */ + pointer_control = memcmp((const char *)(gb.rom + 0x134), "LEGO ATEAM BLPP", strlen("LEGO ATEAM BLPP")) == 0; + push_faster |= pointer_control; + /* Run emulation */ running = true; gb.turbo = gb.turbo_dont_skip = gb.disable_rendering = true; frames = 0; + unsigned cycles = 0; while (running) { - GB_run(&gb); + cycles += GB_run(&gb); + if (cycles >= 139810) { /* Approximately 1/60 a second. Intentionally not the actual length of a frame. */ + handle_buttons(&gb); + cycles -= 139810; + frames++; + } /* This early crash test must not run in vblank because PC might not point to the next instruction. */ if (gb.pc == 0x38 && frames < test_length - 1 && GB_read_memory(&gb, 0x38) == 0xFF) { GB_log(&gb, "The game is probably stuck in an FF loop.\n"); @@ -376,6 +418,7 @@ int main(int argc, char **argv) } } + if (log_file) { fclose(log_file); log_file = NULL; @@ -390,7 +433,7 @@ int main(int argc, char **argv) } #ifndef _WIN32 int wait_out; - while(wait(&wait_out) != -1); + while (wait(&wait_out) != -1); #endif return 0; } diff --git a/Windows/resources.rc b/Windows/resources.rc index ffa8b3b..73c1213 100644 Binary files a/Windows/resources.rc and b/Windows/resources.rc differ diff --git a/Windows/stdio.h b/Windows/stdio.h index d68c956..ef21ea4 100755 --- a/Windows/stdio.h +++ b/Windows/stdio.h @@ -2,6 +2,10 @@ #include_next #include +int access(const char *filename, int mode); +#define R_OK 2 +#define W_OK 4 + #ifndef __MINGW32__ #ifndef __LIBRETRO__ static inline int vasprintf(char **str, const char *fmt, va_list args) @@ -20,7 +24,8 @@ 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; size_t size; @@ -72,4 +77,4 @@ static inline size_t getline(char **lineptr, size_t *n, FILE *stream) { return p - bufptr - 1; } -#define snprintf _snprintf \ No newline at end of file +#define snprintf _snprintf diff --git a/Windows/unistd.h b/Windows/unistd.h new file mode 100644 index 0000000..b7aabf2 --- /dev/null +++ b/Windows/unistd.h @@ -0,0 +1,7 @@ +#include +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 + +#define read(...) _read(__VA_ARGS__) +#define write(...) _write(__VA_ARGS__) diff --git a/Windows/utf8_compat.c b/Windows/utf8_compat.c index 1005f22..0347211 100755 --- a/Windows/utf8_compat.c +++ b/Windows/utf8_compat.c @@ -1,6 +1,7 @@ #include #include #include +#include FILE *fopen(const char *filename, const char *mode) { @@ -11,4 +12,13 @@ FILE *fopen(const char *filename, const char *mode) MultiByteToWideChar(CP_UTF8, 0, mode, -1, w_mode, sizeof(w_mode) / sizeof(w_mode[0])); return _wfopen(w_filename, w_mode); -} \ No newline at end of file +} + +int access(const char *filename, int mode) +{ + wchar_t w_filename[MAX_PATH] = {0,}; + MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, sizeof(w_filename) / sizeof(w_filename[0])); + + return _waccess(w_filename, mode); +} + diff --git a/build-faq.md b/build-faq.md index e2192f5..2b056dd 100644 --- a/build-faq.md +++ b/build-faq.md @@ -4,4 +4,4 @@ When building on macOS, the build system will make a native Cocoa app by default # Attempting to build the SDL frontend on macOS fails on linking -SameBoy on macOS expects you to have the SDL2 framework installed as a framework. You can find the SDL2 binaries on the [SDL homepage](https://www.libsdl.org/download-2.0.php). Mount the DMG and copy SDL2.framework to `/Library/Frameworks/`. +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 diff --git a/libretro/Makefile b/libretro/Makefile index 75ddfc6..b327628 100644 --- a/libretro/Makefile +++ b/libretro/Makefile @@ -1,6 +1,8 @@ STATIC_LINKING := 0 AR := ar +CFLAGS := -Wall $(CFLAGS) + GIT_VERSION ?= " $(shell git rev-parse --short HEAD || echo unknown)" ifneq ($(GIT_VERSION)," unknown") CFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\" @@ -93,7 +95,7 @@ else ifeq ($(platform), linux-portable) else ifeq ($(platform), switch) TARGET := $(TARGET_NAME)_libretro_$(platform).a include $(LIBTRANSISTOR_HOME)/libtransistor.mk - CFLAGS += -Wl,-q -Wall -O3 -fno-short-enums -fno-optimize-sibling-calls + CFLAGS += -Wl,-q -O3 -fno-short-enums -fno-optimize-sibling-calls STATIC_LINKING=1 # Nintendo WiiU else ifeq ($(platform), wiiu) @@ -141,7 +143,7 @@ else ifeq ($(platform), vita) TARGET := $(TARGET_NAME)_vita.a CC = arm-vita-eabi-gcc AR = arm-vita-eabi-ar - CFLAGS += -Wl,-q -Wall -O3 -fno-short-enums -fno-optimize-sibling-calls + CFLAGS += -Wl,-q -O3 -fno-short-enums -fno-optimize-sibling-calls STATIC_LINKING = 1 # Windows MSVC 2017 all architectures @@ -260,7 +262,7 @@ endif include Makefile.common -OBJECTS := $(patsubst %.c,$(CORE_DIR)/build/obj/%_libretro.c.o,$(SOURCES_C)) +OBJECTS := $(patsubst $(CORE_DIR)/%.c,$(CORE_DIR)/build/obj/%_libretro.c.o,$(SOURCES_C)) OBJOUT = -o LINKOUT = -o @@ -278,7 +280,7 @@ else LD = $(CC) endif -CFLAGS += -Wall -D__LIBRETRO__ $(fpic) $(INCFLAGS) -std=gnu11 -D_GNU_SOURCE -D_USE_MATH_DEFINES +CFLAGS += -D__LIBRETRO__ $(fpic) $(INCFLAGS) -std=gnu11 -D_GNU_SOURCE -D_USE_MATH_DEFINES all: $(TARGET) @@ -306,7 +308,7 @@ else $(LD) $(fpic) $(SHARED) $(INCFLAGS) $(LINKOUT)$@ $(OBJECTS) $(LDFLAGS) endif -$(CORE_DIR)/build/obj/%_libretro.c.o: %.c +$(CORE_DIR)/build/obj/%_libretro.c.o: $(CORE_DIR)/%.c -@$(MKDIR) -p $(dir $@) $(CC) -c $(OBJOUT)$@ $< $(CFLAGS) $(fpic) -DGB_INTERNAL diff --git a/libretro/Makefile.common b/libretro/Makefile.common index fb3a02f..7f7688a 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -12,6 +12,8 @@ SOURCES_C := $(CORE_DIR)/Core/gb.c \ $(CORE_DIR)/Core/sm83_cpu.c \ $(CORE_DIR)/Core/joypad.c \ $(CORE_DIR)/Core/save_state.c \ + $(CORE_DIR)/Core/random.c \ + $(CORE_DIR)/Core/rumble.c \ $(CORE_DIR)/libretro/agb_boot.c \ $(CORE_DIR)/libretro/cgb_boot.c \ $(CORE_DIR)/libretro/dmg_boot.c \ @@ -19,7 +21,7 @@ SOURCES_C := $(CORE_DIR)/Core/gb.c \ $(CORE_DIR)/libretro/sgb2_boot.c \ $(CORE_DIR)/libretro/libretro.c -CFLAGS += -DDISABLE_TIMEKEEPING -DDISABLE_REWIND -DDISABLE_DEBUGGER +CFLAGS += -DGB_DISABLE_TIMEKEEPING -DGB_DISABLE_REWIND -DGB_DISABLE_DEBUGGER -DGB_DISABLE_CHEATS SOURCES_CXX := diff --git a/libretro/jni/Application.mk b/libretro/jni/Application.mk index 219fdf2..a252a72 100644 --- a/libretro/jni/Application.mk +++ b/libretro/jni/Application.mk @@ -1,2 +1 @@ -APP_STL := gnustl_static APP_ABI := all diff --git a/libretro/libretro.c b/libretro/libretro.c index 937c6e8..24514d4 100644 --- a/libretro/libretro.c +++ b/libretro/libretro.c @@ -14,8 +14,6 @@ #define AUDIO_FREQUENCY 48000 #endif -#define FRAME_RATE (0x400000 / 70224.0) - #ifdef _WIN32 #include #include @@ -31,13 +29,10 @@ static const char slash = '\\'; static const char slash = '/'; #endif -#define VIDEO_WIDTH 160 -#define VIDEO_HEIGHT 144 -#define VIDEO_PIXELS (VIDEO_WIDTH * VIDEO_HEIGHT) +#define MAX_VIDEO_WIDTH 256 +#define MAX_VIDEO_HEIGHT 224 +#define MAX_VIDEO_PIXELS (MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT) -#define SGB_VIDEO_WIDTH 256 -#define SGB_VIDEO_HEIGHT 224 -#define SGB_VIDEO_PIXELS (SGB_VIDEO_WIDTH * SGB_VIDEO_HEIGHT) #define RETRO_MEMORY_GAMEBOY_1_SRAM ((1 << 8) | RETRO_MEMORY_SAVE_RAM) #define RETRO_MEMORY_GAMEBOY_1_RTC ((2 << 8) | RETRO_MEMORY_RTC) @@ -86,7 +81,7 @@ static struct retro_log_callback logging; static retro_log_printf_t log_cb; static retro_video_refresh_t video_cb; -static retro_audio_sample_batch_t audio_batch_cb; +static retro_audio_sample_t audio_sample_cb; static retro_input_poll_t input_poll_cb; static retro_input_state_t input_state_cb; @@ -143,41 +138,36 @@ static void GB_update_keys_status(GB_gameboy_t *gb, unsigned port) 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)); - if (gb->rumble_state) - rumble.set_rumble_state(port, RETRO_RUMBLE_STRONG, 65535); - else - rumble.set_rumble_state(port, RETRO_RUMBLE_STRONG, 0); } - -static void audio_callback(void *gb) +static void rumble_callback(GB_gameboy_t *gb, double amplitude) { - size_t length = GB_apu_get_current_buffer_length(gb); - - while (length > sizeof(soundbuf) / 4) - { - GB_apu_copy_buffer(gb, (GB_sample_t *) soundbuf, 1024); - audio_batch_cb(soundbuf, 1024); - length -= 1024; + if (!rumble.set_rumble_state) return; + + if (gb == &gameboy[0]) { + rumble.set_rumble_state(0, RETRO_RUMBLE_STRONG, 65535 * amplitude); } - if (length) { - GB_apu_copy_buffer(gb, (GB_sample_t *) soundbuf, length); - audio_batch_cb(soundbuf, length); + else if (gb == &gameboy[1]) { + rumble.set_rumble_state(1, RETRO_RUMBLE_STRONG, 65535 * amplitude); + } +} + +static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) +{ + if ((audio_out == GB_1 && gb == &gameboy[0]) || + (audio_out == GB_2 && gb == &gameboy[1])) { + audio_sample_cb(sample->left, sample->right); } } static void vblank1(GB_gameboy_t *gb) { vblank1_occurred = true; - if (audio_out == GB_1) - audio_callback(gb); } static void vblank2(GB_gameboy_t *gb) { vblank2_occurred = true; - if (audio_out == GB_2) - audio_callback(gb); } static bool bit_to_send1 = true, bit_to_send2 = true; @@ -208,16 +198,18 @@ static bool serial_end2(GB_gameboy_t *gb) static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { - return r<<16|g<<8|b; + return r <<16 | g <<8 | 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; off|correct curves|emulate hardware|preserve brightness" }, - { "sameboy_high_pass_filter_mode", "High-pass filter; off|accurate|remove dc offset" }, + { "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 } }; @@ -229,10 +221,12 @@ static const struct retro_variable vars_dual[] = { { "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; off|correct curves|emulate hardware|preserve brightness" }, - { "sameboy_color_correction_mode_2", "Color correction for Game Boy #2; off|correct curves|emulate hardware|preserve brightness" }, - { "sameboy_high_pass_filter_mode_1", "High-pass filter for Game Boy #1; off|accurate|remove dc offset" }, - { "sameboy_high_pass_filter_mode_2", "High-pass filter for Game Boy #2; off|accurate|remove dc offset" }, + { "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 } }; @@ -335,15 +329,13 @@ 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); } - 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); @@ -351,6 +343,119 @@ static void set_link_cable_state(bool state) } } +static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) +{ + const char *model_name = (char *[]){ + [GB_BOOT_ROM_DMG0] = "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] = "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 + [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] = cgb_boot, + [GB_BOOT_ROM_AGB] = agb_boot, + }[type]; + + unsigned boot_length = (unsigned []){ + [GB_BOOT_ROM_DMG0] = dmg_boot_length, // dmg0 not implemented yet + [GB_BOOT_ROM_DMG] = dmg_boot_length, + [GB_BOOT_ROM_MGB] = dmg_boot_length, // mgb not implemented yet + [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] = 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); + log_cb(RETRO_LOG_INFO, "Loading boot image: %s\n", buf); + + if (GB_load_boot_rom(gb, buf)) { + GB_load_boot_rom_from_buffer(gb, boot_code, boot_length); + } +} + +static void retro_set_memory_maps(void) +{ + struct retro_memory_descriptor descs[11]; + size_t size; + uint16_t bank; + unsigned i; + + + /* todo: add netplay awareness for this so achievements can be granted on the respective client */ + i = 0; + memset(descs, 0, sizeof(descs)); + + descs[0].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_IE, &size, &bank); + descs[0].start = 0xFFFF; + descs[0].len = 1; + + descs[1].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_HRAM, &size, &bank); + descs[1].start = 0xFF80; + descs[1].len = 0x0080; + + descs[2].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_RAM, &size, &bank); + descs[2].start = 0xC000; + descs[2].len = 0x1000; + + descs[3].ptr = descs[2].ptr + 0x1000; /* GB RAM/GBC RAM bank 1 */ + descs[3].start = 0xD000; + descs[3].len = 0x1000; + + descs[4].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_CART_RAM, &size, &bank); + descs[4].start = 0xA000; + descs[4].len = 0x2000; + + descs[5].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_VRAM, &size, &bank); + descs[5].start = 0x8000; + descs[5].len = 0x2000; + + descs[6].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_ROM, &size, &bank); + descs[6].start = 0x0000; + descs[6].len = 0x4000; + descs[6].flags = RETRO_MEMDESC_CONST; + + descs[7].ptr = descs[6].ptr + (bank * 0x4000); + descs[7].start = 0x4000; + descs[7].len = 0x4000; + descs[7].flags = RETRO_MEMDESC_CONST; + + descs[8].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_OAM, &size, &bank); + descs[8].start = 0xFE00; + descs[8].len = 0x00A0; + descs[8].select = 0xFFFFFF00; + + descs[9].ptr = descs[2].ptr + 0x2000; /* GBC RAM bank 2 */ + descs[9].start = 0x10000; + descs[9].len = GB_is_cgb(&gameboy[i]) ? 0x6000 : 0; /* 0x1000 per bank (2-7), unmapped on GB */ + descs[9].select = 0xFFFF0000; + + descs[10].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_IO, &size, &bank); + descs[10].start = 0xFF00; + descs[10].len = 0x0080; + descs[10].select = 0xFFFFFF00; + + struct retro_memory_map mmaps; + mmaps.descriptors = descs; + mmaps.num_descriptors = sizeof(descs) / sizeof(descs[0]); + environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, &mmaps); +} + static void init_for_current_model(unsigned id) { unsigned i = id; @@ -368,90 +473,29 @@ static void init_for_current_model(unsigned id) else { GB_init(&gameboy[i], libretro_to_internal_model[effective_model]); } - const char *model_name = (const char *[]){"dmg", "cgb", "agb", "sgb", "sgb2"}[effective_model]; - const unsigned char *boot_code = (const unsigned char *[]){dmg_boot, cgb_boot, agb_boot, sgb_boot, sgb2_boot}[effective_model]; - unsigned boot_length = (unsigned []){dmg_boot_length, cgb_boot_length, agb_boot_length, sgb_boot_length, sgb2_boot_length}[effective_model]; - 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); - log_cb(RETRO_LOG_INFO, "Loading boot image: %s\n", buf); - if (GB_load_boot_rom(&gameboy[i], buf)) - GB_load_boot_rom_from_buffer(&gameboy[i], boot_code, boot_length); - GB_set_user_data(&gameboy[i], (void*)NULL); + GB_set_boot_rom_load_callback(&gameboy[i], boot_rom_load); - GB_set_pixels_output(&gameboy[i],(unsigned int*)(frame_buf + i * ((model[i] == MODEL_SGB || model[i] == MODEL_SGB2) ? SGB_VIDEO_PIXELS : VIDEO_PIXELS))); + /* When running multiple devices they are assumed to use the same resolution */ + + GB_set_pixels_output(&gameboy[i], + (uint32_t *)(frame_buf + GB_get_screen_width(&gameboy[0]) * GB_get_screen_height(&gameboy[0]) * i)); GB_set_rgb_encode_callback(&gameboy[i], rgb_encode); GB_set_sample_rate(&gameboy[i], AUDIO_FREQUENCY); + GB_apu_set_sample_callback(&gameboy[i], audio_callback); + GB_set_rumble_callback(&gameboy[i], rumble_callback); /* todo: attempt to make these more generic */ GB_set_vblank_callback(&gameboy[0], (GB_vblank_callback_t) vblank1); - if (emulated_devices == 2) - { + if (emulated_devices == 2) { GB_set_vblank_callback(&gameboy[1], (GB_vblank_callback_t) vblank2); - if (link_cable_emulation) + if (link_cable_emulation) { set_link_cable_state(true); + } } - struct retro_memory_descriptor descs[10]; - size_t size; - uint16_t bank; - - - /* todo: add netplay awareness for this so achievements can be granted on the respective client */ - i = 0; - memset(descs, 0, sizeof(descs)); - - descs[0].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_IE, &size, &bank); - descs[0].start = 0xFFFF; - descs[0].len = 1; - - descs[1].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_HRAM, &size, &bank); - descs[1].start = 0xFF80; - descs[1].len = 0x0080; - - descs[2].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_RAM, &size, &bank); - descs[2].start = 0xC000; - descs[2].len = 0x1000; - - descs[3].ptr = descs[2].ptr + (bank * 0x1000); - descs[3].start = 0xD000; - descs[3].len = 0x1000; - - descs[4].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_CART_RAM, &size, &bank); - descs[4].start = 0xA000; - descs[4].len = 0x2000; - - descs[5].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_VRAM, &size, &bank); - descs[5].start = 0x8000; - descs[5].len = 0x2000; - - descs[6].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_ROM, &size, &bank); - descs[6].start = 0x0000; - descs[6].len = 0x4000; - descs[6].flags = RETRO_MEMDESC_CONST; - - descs[7].ptr = descs[6].ptr + (bank * 0x4000); - descs[7].start = 0x4000; - descs[7].len = 0x4000; - descs[7].flags = RETRO_MEMDESC_CONST; - - descs[8].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_OAM, &size, &bank); - descs[8].start = 0xFE00; - descs[8].len = 0x00A0; - - descs[9].ptr = descs[2].ptr + 0x1000; - descs[9].start = 0x10000; - descs[9].len = GB_is_cgb(&gameboy[i]) ? 0x7000 : 0; - - struct retro_memory_map mmaps; - mmaps.descriptors = descs; - mmaps.num_descriptors = sizeof(descs) / sizeof(descs[0]); - environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, &mmaps); - /* 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 || model[0] == MODEL_SGB2)) { static const struct retro_controller_info ports[] = { { controllers_sgb, 1 }, { controllers_sgb, 1 }, @@ -462,8 +506,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 }, @@ -471,8 +514,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 }, @@ -487,134 +529,225 @@ static void init_for_current_model(unsigned id) static void check_variables() { struct retro_variable var = {0}; - if (emulated_devices == 1) - { + if (emulated_devices == 1) { var.key = "sameboy_color_correction_mode"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && GB_is_cgb(&gameboy[0])) - { - if (strcmp(var.value, "off") == 0) + 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) + } + 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) + } + 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) + } + 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); + } + } + + var.key = "sameboy_rumble"; + 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) + 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) + } + 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) + } + 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) - { + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { enum model new_model = model[0]; - if (strcmp(var.value, "Game Boy") == 0) + if (strcmp(var.value, "Game Boy") == 0) { new_model = MODEL_DMG; - else if (strcmp(var.value, "Game Boy Color") == 0) + } + else if (strcmp(var.value, "Game Boy Color") == 0) { new_model = MODEL_CGB; - else if (strcmp(var.value, "Game Boy Advance") == 0) + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { new_model = MODEL_AGB; - else if (strcmp(var.value, "Super Game Boy") == 0) + } + else if (strcmp(var.value, "Super Game Boy") == 0) { new_model = MODEL_SGB; - else if (strcmp(var.value, "Super Game Boy 2") == 0) + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { new_model = MODEL_SGB2; - else + } + else { new_model = MODEL_AUTO; + } - if (new_model != model[0]) - { + if (new_model != model[0]) { geometry_updated = true; model[0] = new_model; init_for_current_model(0); } } + + var.key = "sameboy_border"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "never") == 0) { + GB_set_border_mode(&gameboy[0], GB_BORDER_NEVER); + } + else if (strcmp(var.value, "Super Game Boy only") == 0) { + GB_set_border_mode(&gameboy[0], GB_BORDER_SGB); + } + else if (strcmp(var.value, "always") == 0) { + GB_set_border_mode(&gameboy[0], GB_BORDER_ALWAYS); + } + + geometry_updated = true; + } } - else - { + 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.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && GB_is_cgb(&gameboy[0])) - { - if (strcmp(var.value, "off") == 0) + 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) + } + 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) + } + 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) + } + 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); + } } var.key = "sameboy_color_correction_mode_2"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value && GB_is_cgb(&gameboy[1])) - { - if (strcmp(var.value, "off") == 0) + 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) + } + 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) + } + 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) + } + 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); + } + + } + + 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); + } } 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) + 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) + } + 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) + } + 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) + 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) + } + 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) + } + 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) - { + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { enum model new_model = model[0]; - if (strcmp(var.value, "Game Boy") == 0) + if (strcmp(var.value, "Game Boy") == 0) { new_model = MODEL_DMG; - else if (strcmp(var.value, "Game Boy Color") == 0) + } + else if (strcmp(var.value, "Game Boy Color") == 0) { new_model = MODEL_CGB; - else if (strcmp(var.value, "Game Boy Advance") == 0) + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { new_model = MODEL_AGB; - else if (strcmp(var.value, "Super Game Boy") == 0) + } + else if (strcmp(var.value, "Super Game Boy") == 0) { new_model = MODEL_SGB; - else if (strcmp(var.value, "Super Game Boy 2") == 0) + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { new_model = MODEL_SGB2; - else + } + else { new_model = MODEL_AUTO; + } - if (model[0] != new_model) - { + if (model[0] != new_model) { model[0] = new_model; init_for_current_model(0); } @@ -622,24 +755,28 @@ static void check_variables() var.key = "sameboy_model_2"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) - { + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { enum model new_model = model[1]; - if (strcmp(var.value, "Game Boy") == 0) + if (strcmp(var.value, "Game Boy") == 0) { new_model = MODEL_DMG; - else if (strcmp(var.value, "Game Boy Color") == 0) + } + else if (strcmp(var.value, "Game Boy Color") == 0) { new_model = MODEL_CGB; - else if (strcmp(var.value, "Game Boy Advance") == 0) + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { new_model = MODEL_AGB; - else if (strcmp(var.value, "Super Game Boy") == 0) + } + else if (strcmp(var.value, "Super Game Boy") == 0) { new_model = MODEL_SGB; - else if (strcmp(var.value, "Super Game Boy 2") == 0) + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { new_model = MODEL_SGB; - else + } + else { new_model = MODEL_AUTO; + } - if (model[1] != new_model) - { + if (model[1] != new_model) { model[1] = new_model; init_for_current_model(1); } @@ -647,39 +784,44 @@ 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) + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "top-down") == 0) { screen_layout = LAYOUT_TOP_DOWN; - else + } + else { screen_layout = LAYOUT_LEFT_RIGHT; + } geometry_updated = true; } 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) + if (strcmp(var.value, "enabled") == 0) { link_cable_emulation = true; - else + } + else { link_cable_emulation = false; - if (link_cable_emulation && link_cable_emulation != tmp) + } + if (link_cable_emulation && link_cable_emulation != tmp) { set_link_cable_state(true); - else if (!link_cable_emulation && link_cable_emulation != tmp) + } + else if (!link_cable_emulation && link_cable_emulation != tmp) { set_link_cable_state(false); + } } var.key = "sameboy_audio_output"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) - { - if (strcmp(var.value, "Game Boy #1") == 0) + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "Game Boy #1") == 0) { audio_out = GB_1; - else + } + else { audio_out = GB_2; + } } } } @@ -688,15 +830,26 @@ void retro_init(void) { const char *dir = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &dir) && dir) + if (environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &dir) && dir) { snprintf(retro_system_directory, sizeof(retro_system_directory), "%s", dir); - else + } + else { snprintf(retro_system_directory, sizeof(retro_system_directory), "%s", "."); + } - if (environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &dir) && dir) + if (environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &dir) && dir) { snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", dir); - else + } + else { snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", "."); + } + + if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &logging)) { + log_cb = logging.log; + } + else { + log_cb = fallback_log; + } } void retro_deinit(void) @@ -733,38 +886,28 @@ void retro_get_system_info(struct retro_system_info *info) void retro_get_system_av_info(struct retro_system_av_info *info) { struct retro_game_geometry geom; - struct retro_system_timing timing = { FRAME_RATE, AUDIO_FREQUENCY }; + 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 = VIDEO_WIDTH; - geom.base_height = VIDEO_HEIGHT * emulated_devices; - geom.aspect_ratio = (double)VIDEO_WIDTH / (emulated_devices * VIDEO_HEIGHT); - }else if (screen_layout == LAYOUT_LEFT_RIGHT) { - geom.base_width = VIDEO_WIDTH * emulated_devices; - geom.base_height = VIDEO_HEIGHT; - geom.aspect_ratio = ((double)VIDEO_WIDTH * emulated_devices) / VIDEO_HEIGHT; + geom.base_width = GB_get_screen_width(&gameboy[0]); + geom.base_height = GB_get_screen_height(&gameboy[0]) * emulated_devices; + geom.aspect_ratio = (double)GB_get_screen_width(&gameboy[0]) / (emulated_devices * GB_get_screen_height(&gameboy[0])); + } + else if (screen_layout == LAYOUT_LEFT_RIGHT) { + geom.base_width = GB_get_screen_width(&gameboy[0]) * emulated_devices; + geom.base_height = GB_get_screen_height(&gameboy[0]); + geom.aspect_ratio = ((double)GB_get_screen_width(&gameboy[0]) * emulated_devices) / GB_get_screen_height(&gameboy[0]); } } - else - { - if (model[0] == MODEL_SGB || model[0] == MODEL_SGB2) - { - geom.base_width = SGB_VIDEO_WIDTH; - geom.base_height = SGB_VIDEO_HEIGHT; - geom.aspect_ratio = (double)SGB_VIDEO_WIDTH / SGB_VIDEO_HEIGHT; - } - else - { - geom.base_width = VIDEO_WIDTH; - geom.base_height = VIDEO_HEIGHT; - geom.aspect_ratio = (double)VIDEO_WIDTH / VIDEO_HEIGHT; - } + 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]); } - geom.max_width = SGB_VIDEO_WIDTH * emulated_devices; - geom.max_height = SGB_VIDEO_HEIGHT * emulated_devices; + geom.max_width = MAX_VIDEO_WIDTH * emulated_devices; + geom.max_height = MAX_VIDEO_HEIGHT * emulated_devices; info->geometry = geom; info->timing = timing; @@ -775,21 +918,16 @@ void retro_set_environment(retro_environment_t cb) { environ_cb = cb; - if (cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &logging)) - log_cb = logging.log; - else - log_cb = fallback_log; - cb(RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO, (void*)subsystems); } void retro_set_audio_sample(retro_audio_sample_t cb) { + audio_sample_cb = cb; } void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { - audio_batch_cb = cb; } void retro_set_input_poll(retro_input_poll_t cb) @@ -809,8 +947,9 @@ void retro_set_video_refresh(retro_video_refresh_t cb) void retro_reset(void) { - for (int i = 0; i < emulated_devices; i++) + for (int i = 0; i < emulated_devices; i++) { GB_reset(&gameboy[i]); + } } @@ -819,8 +958,9 @@ void retro_run(void) bool updated = false; - if (!initialized) + if (!initialized) { geometry_updated = false; + } if (geometry_updated) { struct retro_system_av_info info; @@ -829,29 +969,30 @@ void retro_run(void) geometry_updated = false; } - if (!frame_buf) + if (!frame_buf) { return; + } - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated) + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated) { 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)) - { - for (unsigned i = 0; i < 4; i++) + else if (emulated_devices == 1 && (model[0] == MODEL_SGB || model[0] == MODEL_SGB2)) { + for (unsigned i = 0; i < 4; i++) { GB_update_keys_status(&gameboy[0], i); + } } - else + else { GB_update_keys_status(&gameboy[0], 0); + } 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]); @@ -861,35 +1002,36 @@ void retro_run(void) } } } - else - { - int x = GB_run_frame(&gameboy[0]); - log_cb(RETRO_LOG_DEBUG, "%d\n", x); + else { + GB_run_frame(&gameboy[0]); } - if (emulated_devices == 2) - { + if (emulated_devices == 2) { if (screen_layout == LAYOUT_TOP_DOWN) { - video_cb(frame_buf, VIDEO_WIDTH, VIDEO_HEIGHT * emulated_devices, VIDEO_WIDTH * sizeof(uint32_t)); - }else if (screen_layout == LAYOUT_LEFT_RIGHT) { - /* use slow memcpy method for now */ - for (int index = 0; index < emulated_devices; index++) { - for (int y = 0; y < VIDEO_HEIGHT; y++) { - for (int x = 0; x < VIDEO_WIDTH; x++) { - frame_buf_copy[VIDEO_WIDTH * emulated_devices * y + (x + VIDEO_WIDTH * index)] = frame_buf[VIDEO_WIDTH * (y + VIDEO_HEIGHT * index) + x]; - } + video_cb(frame_buf, + GB_get_screen_width(&gameboy[0]), + GB_get_screen_height(&gameboy[0]) * emulated_devices, + GB_get_screen_width(&gameboy[0]) * sizeof(uint32_t)); + } + else if (screen_layout == LAYOUT_LEFT_RIGHT) { + unsigned pitch = GB_get_screen_width(&gameboy[0]) * emulated_devices; + unsigned pixels_per_device = GB_get_screen_width(&gameboy[0]) * GB_get_screen_height(&gameboy[0]); + for (int y = 0; y < GB_get_screen_height(&gameboy[0]); y++) { + for (unsigned i = 0; i < emulated_devices; i++) { + memcpy(frame_buf_copy + y * pitch + GB_get_screen_width(&gameboy[0]) * i, + frame_buf + pixels_per_device * i + y * GB_get_screen_width(&gameboy[0]), + GB_get_screen_width(&gameboy[0]) * sizeof(uint32_t)); } } - video_cb(frame_buf_copy, VIDEO_WIDTH * emulated_devices, VIDEO_HEIGHT, VIDEO_WIDTH * emulated_devices * sizeof(uint32_t)); + video_cb(frame_buf_copy, GB_get_screen_width(&gameboy[0]) * emulated_devices, GB_get_screen_height(&gameboy[0]), GB_get_screen_width(&gameboy[0]) * emulated_devices * sizeof(uint32_t)); } } - else - { - if (model[0] == MODEL_SGB || model[0] == MODEL_SGB2) - video_cb(frame_buf, SGB_VIDEO_WIDTH, SGB_VIDEO_HEIGHT, SGB_VIDEO_WIDTH * sizeof(uint32_t)); - else - video_cb(frame_buf, VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_WIDTH * sizeof(uint32_t)); + else { + video_cb(frame_buf, + GB_get_screen_width(&gameboy[0]), + GB_get_screen_height(&gameboy[0]), + GB_get_screen_width(&gameboy[0]) * sizeof(uint32_t)); } @@ -901,12 +1043,11 @@ 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(SGB_VIDEO_PIXELS* emulated_devices * sizeof(uint32_t)); - memset(frame_buf, 0, SGB_VIDEO_PIXELS * emulated_devices * sizeof(uint32_t)); + 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; } @@ -914,11 +1055,9 @@ bool retro_load_game(const struct retro_game_info *info) auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB : MODEL_DMG; 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; } @@ -927,19 +1066,25 @@ bool retro_load_game(const struct retro_game_info *info) bool achievements = true; environ_cb(RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS, &achievements); - if (environ_cb(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumble)) + if (environ_cb(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumble)) { log_cb(RETRO_LOG_INFO, "Rumble environment supported\n"); - else + } + else { log_cb(RETRO_LOG_INFO, "Rumble environment not supported\n"); + } check_variables(); + + retro_set_memory_maps(); + return true; } void retro_unload_game(void) { - for (int i = 0; i < emulated_devices; i++) + for (int i = 0; i < emulated_devices; i++) { GB_free(&gameboy[i]); + } } unsigned retro_get_region(void) @@ -950,23 +1095,24 @@ unsigned retro_get_region(void) bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t num_info) { - if (type == RETRO_GAME_TYPE_GAMEBOY_LINK_2P) + if (type == RETRO_GAME_TYPE_GAMEBOY_LINK_2P) { emulated_devices = 2; - else + } + else { 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 * VIDEO_PIXELS * sizeof(uint32_t)); - frame_buf_copy = (uint32_t*)malloc(emulated_devices * VIDEO_PIXELS * sizeof(uint32_t)); + frame_buf = (uint32_t*)malloc(emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); + frame_buf_copy = (uint32_t*)malloc(emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); - memset(frame_buf, 0, emulated_devices * VIDEO_PIXELS * sizeof(uint32_t)); - memset(frame_buf_copy, 0, emulated_devices * VIDEO_PIXELS * sizeof(uint32_t)); + memset(frame_buf, 0, emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); + 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; } @@ -974,11 +1120,9 @@ bool retro_load_game_special(unsigned type, const struct retro_game_info *info, auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB : MODEL_DMG; 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; } @@ -987,10 +1131,12 @@ bool retro_load_game_special(unsigned type, const struct retro_game_info *info, bool achievements = true; environ_cb(RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS, &achievements); - if (environ_cb(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumble)) + if (environ_cb(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumble)) { log_cb(RETRO_LOG_INFO, "Rumble environment supported\n"); - else + } + else { log_cb(RETRO_LOG_INFO, "Rumble environment not supported\n"); + } check_variables(); return true; @@ -998,54 +1144,65 @@ bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t retro_serialize_size(void) { - if (emulated_devices == 2) - return GB_get_save_state_size(&gameboy[0]) + GB_get_save_state_size(&gameboy[1]); - else - return GB_get_save_state_size(&gameboy[0]); + static size_t maximum_save_size = 0; + 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; } bool retro_serialize(void *data, size_t size) { - if (!initialized) + if (!initialized || !data) { return false; - - void* save_data[2]; - size_t state_size[2]; - size_t offset = 0; - - for (int i = 0; i < emulated_devices; i++) - { - state_size[i] = GB_get_save_state_size(&gameboy[i]); - save_data[i] = (uint8_t*)malloc(state_size[i]); - GB_save_state_to_buffer(&gameboy[i], (uint8_t*) save_data[i]); - memcpy(data + offset, save_data[i], state_size[i]); - offset += state_size[i]; - free(save_data[i]); } - if (data) - return true; - else - return false; + size_t offset = 0; + + 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; + } + + GB_save_state_to_buffer(&gameboy[i], ((uint8_t *) data) + offset); + offset += state_size; + size -= state_size; + } + + return true; } bool retro_unserialize(const void *data, size_t size) { - void* save_data[2]; - size_t state_size[2]; - int ret; - - for (int i = 0; i < emulated_devices; i++) - { - state_size[i] = GB_get_save_state_size(&gameboy[i]); - save_data[i] = (uint8_t*)malloc(state_size[i]); - memcpy (save_data[i], data + (state_size[i] * i), state_size[i]); - ret = GB_load_state_from_buffer(&gameboy[i], save_data[i], state_size[i]); - free(save_data[i]); - - if (ret != 0) + 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; + } + + if (GB_load_state_from_buffer(&gameboy[i], data, state_size)) { + return false; + } + + size -= state_size; + data = ((uint8_t *)data) + state_size; } return true; @@ -1055,59 +1212,67 @@ 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; case RETRO_MEMORY_SAVE_RAM: - if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { data = gameboy[0].mbc_ram; - else + } + else { data = NULL; + } break; case RETRO_MEMORY_VIDEO_RAM: data = gameboy[0].vram; break; case RETRO_MEMORY_RTC: - if(gameboy[0].cartridge_type->has_battery) + if (gameboy[0].cartridge_type->has_battery) { data = GB_GET_SECTION(&gameboy[0], rtc); - else + } + else { data = NULL; + } break; default: 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) + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { data = gameboy[0].mbc_ram; - else + } + else { data = NULL; + } break; case RETRO_MEMORY_GAMEBOY_2_SRAM: - if (gameboy[1].cartridge_type->has_battery && gameboy[1].mbc_ram_size != 0) + if (gameboy[1].cartridge_type->has_battery && gameboy[1].mbc_ram_size != 0) { data = gameboy[1].mbc_ram; - else + } + else { data = NULL; + } break; case RETRO_MEMORY_GAMEBOY_1_RTC: - if(gameboy[0].cartridge_type->has_battery) + if (gameboy[0].cartridge_type->has_battery) { data = GB_GET_SECTION(&gameboy[0], rtc); - else + } + else { data = NULL; + } break; case RETRO_MEMORY_GAMEBOY_2_RTC: - if(gameboy[1].cartridge_type->has_battery) + if (gameboy[1].cartridge_type->has_battery) { data = GB_GET_SECTION(&gameboy[1], rtc); - else + } + else { data = NULL; + } break; default: break; @@ -1120,55 +1285,61 @@ 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; case RETRO_MEMORY_SAVE_RAM: - if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { size = gameboy[0].mbc_ram_size; - else + } + else { size = 0; + } break; case RETRO_MEMORY_VIDEO_RAM: size = gameboy[0].vram_size; break; case RETRO_MEMORY_RTC: - if(gameboy[0].cartridge_type->has_battery) + if (gameboy[0].cartridge_type->has_battery) { size = GB_SECTION_SIZE(rtc); - else + } + else { size = 0; + } break; default: 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) + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { size = gameboy[0].mbc_ram_size; - else + } + else { size = 0; + } break; case RETRO_MEMORY_GAMEBOY_2_SRAM: - if (gameboy[1].cartridge_type->has_battery && gameboy[1].mbc_ram_size != 0) + if (gameboy[1].cartridge_type->has_battery && gameboy[1].mbc_ram_size != 0) { size = gameboy[1].mbc_ram_size; - else + } + else { size = 0; + } break; case RETRO_MEMORY_GAMEBOY_1_RTC: - if(gameboy[0].cartridge_type->has_battery) + if (gameboy[0].cartridge_type->has_battery) { size = GB_SECTION_SIZE(rtc); + } break; case RETRO_MEMORY_GAMEBOY_2_RTC: - if(gameboy[1].cartridge_type->has_battery) + if (gameboy[1].cartridge_type->has_battery) { size = GB_SECTION_SIZE(rtc); + } break; default: break;