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 index 4bc9706..c7cfc08 100644 Binary files a/BootROMs/SameBoyLogo.png and b/BootROMs/SameBoyLogo.png differ diff --git a/BootROMs/cgb_boot.asm b/BootROMs/cgb_boot.asm index 0472cbe..dc3544f 100644 --- a/BootROMs/cgb_boot.asm +++ b/BootROMs/cgb_boot.asm @@ -6,22 +6,15 @@ Start: ld sp, $fffe ; Clear memory VRAM - ld hl, $8000 - call ClearMemoryPage + call ClearMemoryPage8000 ld a, 2 ld c, $70 ld [c], a -; Clear RAM Bank 2 (Like the original boot ROM +; Clear RAM Bank 2 (Like the original boot ROM) ld h, $D0 - xor a call ClearMemoryPage ld [c], a -; Clear chosen input palette - ldh [InputPalette], a -; Clear title checksum - ldh [TitleChecksum], a - ; Clear OAM ld h, $fe ld c, $a0 @@ -30,7 +23,19 @@ Start: 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 @@ -39,8 +44,7 @@ Start: ldh [$25], a ld a, $77 ldh [$24], a - - call InitWaveform + ld hl, $FF30 ; Init BG palette ld a, $fc @@ -67,9 +71,7 @@ Start: ; Clear the second VRAM bank ld a, 1 ldh [$4F], a - xor a - ld hl, $8000 - call ClearMemoryPage + call ClearMemoryPage8000 call LoadTileset ld b, 3 @@ -87,20 +89,24 @@ ELSE .tilemapRowLoop + 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 - ; 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 + sub $20 + sub $3 + jr nc, .notspecial + add $20 + call .write_with_palette + dec c +.notspecial pop af - ldi [hl], a - add d + + add d ; d = 3 for SameBoy logo, d = 1 for Nintendo logo dec c jr nz, .tilemapRowLoop - sub 47 + sub 44 push de ld de, $10 add hl, de @@ -116,6 +122,19 @@ ELSE ld l, $a7 ld bc, $0107 jr .tilemapRowLoop + +.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 @@ -125,43 +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 - call LoadBGPalettes64 + call LoadPalettesFromHRAM ; Turn on LCD ld a, $91 @@ -170,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 @@ -183,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 @@ -198,6 +216,9 @@ ELSE call PlaySound ENDC call Preboot +IF DEF(AGB) + ld b, 1 +ENDC ; Will be filled with NOPs @@ -206,7 +227,6 @@ BootGame: ldh [$50], a SECTION "MoreStuff", ROM0[$200] - ; Game Palettes Data TitleChecksums: db $00 ; Default @@ -509,30 +529,31 @@ Palettes: 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 +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 ; Right + A + B - db 52 ; Left + A + B - db 53 ; Up + A + B - db 54 ; Down + A + B + 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.rle" + incbin "SameBoyLogo.pb12" + AnimationColors: dw $7FFF ; White @@ -545,9 +566,6 @@ AnimationColors: dw $7102 ; Blue AnimationColorsEnd: -DMGPalettes: - dw $7FFF, $32BF, $00D0, $0000 - ; Helper Functions DoubleBitsAndWriteRowTwice: call .twice @@ -594,8 +612,11 @@ 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 @@ -634,43 +655,82 @@ ReadCGBLogoHalfTile: ld a, e ret +; 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: -; Copy SameBoy Logo - ld de, SameBoyLogo - ld hl, $8080 -.sameboyLogoLoop - ld a, [de] - inc de - - ld b, a - and $0f - jr z, .skipLiteral - ld c, a - -.literalLoop - ld a, [de] - ldi [hl], a - inc hl - inc de - dec c - jr nz, .literalLoop -.skipLiteral - swap b - ld a, b - and $0f + 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 -.repeatLoop - ldi [hl], a - inc hl - dec c - jr nz, .repeatLoop - jr .sameboyLogoLoop - +; 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 @@ -697,29 +757,6 @@ ReadTrademarkSymbol: jr nz, .loadTrademarkSymbolLoop ret -LoadObjPalettes: - ld c, $6A - jr LoadPalettes - -LoadBGPalettes64: - ld d, 64 - -LoadBGPalettes: - ld e, 0 - 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 - DoIntroAnimation: ; Animate the intro ld a, 1 @@ -759,39 +796,82 @@ IF !DEF(FAST) ld hl, BgPalettes .frameLoop push bc - call BrightenColor + + ; 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 - ld hl, BgPalettes - call LoadBGPalettes64 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 @@ -804,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 @@ -822,6 +902,14 @@ ENDC 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: ld a, 1 ldh [$6C], a ; DMG Emulation @@ -833,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 @@ -846,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 @@ -920,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 @@ -937,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 @@ -949,84 +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 - jp LoadBGPalettes + jr LoadBGPalettes -BrightenColor: +LoadPalettesFromHRAM: + ld hl, BgPalettes + ld d, 64 + +LoadBGPalettes: + ld e, 0 + ld c, $68 + +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 ClearVRAMViaHDMA: - ld hl, $FF51 - - ; Src - ld a, $88 - ld [hli], a - xor a - ld [hli], a - - ; Dest - ld a, $98 - ld [hli], a - ld a, $A0 - ld [hli], a - - ; Do it - ld [hl], $12 + 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 @@ -1034,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 @@ -1051,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? @@ -1094,58 +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 - call LoadBGPalettes64 + 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 @@ -1155,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: diff --git a/BootROMs/dmg_boot.asm b/BootROMs/dmg_boot.asm index 6fb74fb..97a12e7 100644 --- a/BootROMs/dmg_boot.asm +++ b/BootROMs/dmg_boot.asm @@ -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/logo-compress.c b/BootROMs/logo-compress.c deleted file mode 100644 index 2274eb2..0000000 --- a/BootROMs/logo-compress.c +++ /dev/null @@ -1,62 +0,0 @@ -#include -#include -#include -#ifdef _WIN32 -#include -#include -#endif - -void pair(size_t count, uint8_t byte) -{ - static size_t unique_count = 0; - static uint8_t unique_data[15]; - if (count == 1) { - unique_data[unique_count++] = byte; - assert(unique_count <= 15); - } - else { - assert(count <= 15); - uint8_t control = (count << 4) | unique_count; - putchar(control); - - for (size_t i = 0; i < unique_count; i++) { - putchar(unique_data[i]); - } - - if (count != 0) { - putchar(byte); - } - else { - assert(control == 0); - } - - unique_count = 0; - } -} - -int main(int argc, char *argv[]) -{ - size_t count = 1; - uint8_t byte = getchar(); - int new; - size_t position = 0; - -#ifdef _WIN32 - _setmode(0,_O_BINARY); - _setmode(1,_O_BINARY); -#endif - - while ((new = getchar()) != EOF) { - if (byte == new) { - count++; - } - else { - pair(count, byte); - byte = new; - count = 1; - } - } - - pair(count, byte); - pair(0, 0); -} 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/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..9353788 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; @@ -30,6 +33,10 @@ @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 b99e4c8..035c0e2 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -7,6 +7,7 @@ #include "HexFiend/HexFiend.h" #include "GBMemoryByteArray.h" #include "GBWarningPopover.h" +#include "GBCheatWindowController.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!!! */ @@ -61,6 +62,8 @@ enum model { size_t audioBufferSize; size_t audioBufferPosition; size_t audioBufferNeeded; + + bool borderModeChanged; } @property GBAudioClient *audioClient; @@ -74,8 +77,16 @@ enum model { 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); @@ -131,6 +142,12 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [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; @@ -140,7 +157,8 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) NSMutableArray *debugger_input_queue; } -- (instancetype)init { +- (instancetype)init +{ self = [super init]; if (self) { has_debugger_input = [[NSConditionLock alloc] initWithCondition:0]; @@ -184,26 +202,82 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) } } +- (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(), ^{ @@ -244,6 +318,11 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [audioLock unlock]; } +- (void)rumbleChanged:(double)amp +{ + [_view setRumble:amp]; +} + - (void) run { running = true; @@ -257,6 +336,12 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [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)); @@ -273,6 +358,25 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) } 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; @@ -295,6 +399,25 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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; } @@ -312,21 +435,32 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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 @@ -338,8 +472,6 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) current_model = (enum model)[sender tag]; } - [self loadBootROM]; - if (!modelsChanging && [sender tag] == MODEL_NONE) { GB_reset(&gb); } @@ -351,11 +483,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [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 */ @@ -389,6 +517,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) - (void)dealloc { [cameraSession stopRunning]; + self.view.gb = NULL; GB_free(&gb); if (cameraImage) { CVBufferRelease(cameraImage); @@ -398,9 +527,11 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) } } -- (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]]) { @@ -422,7 +553,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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); @@ -435,6 +566,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [self.feedSaveButton removeFromSuperview]; self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[self.fileURL path] lastPathComponent]]; + self.debuggerSplitView.dividerColor = [NSColor clearColor]; /* contentView.superview.subviews.lastObject is the titlebar view */ NSView *titleView = self.printerFeedWindow.contentView.superview.subviews.lastObject; @@ -451,6 +583,26 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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" @@ -536,11 +688,13 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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"; @@ -554,9 +708,18 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) - (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]); }]; @@ -597,15 +760,9 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [[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:)) { @@ -615,9 +772,6 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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; @@ -629,6 +783,9 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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]; } @@ -655,8 +812,8 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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; @@ -737,9 +894,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) } 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]; } @@ -754,7 +909,8 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [self.consoleWindow orderBack:nil]; } -- (IBAction)consoleInput:(NSTextField *)sender { +- (IBAction)consoleInput:(NSTextField *)sender +{ NSString *line = [sender stringValue]; if ([line isEqualToString:@""] && lastConsoleInput) { line = lastConsoleInput; @@ -932,7 +1088,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) { CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef) data); CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; + CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; CGImageRef iref = CGImageCreate(width, @@ -1170,6 +1326,23 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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]; @@ -1375,7 +1548,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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); @@ -1393,9 +1566,9 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) 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: @@ -1472,7 +1645,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) [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 @@ -1520,6 +1693,11 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) } } +- (void) updateFrameBlendingMode +{ + self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; +} + - (void) updateRewindLength { [self performAtomicBlock:^{ @@ -1563,4 +1741,52 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) } +- (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 ae9cf90..81ce018 100644 --- a/Cocoa/Document.xib +++ b/Cocoa/Document.xib @@ -1,19 +1,23 @@ - + - + + + + + @@ -39,22 +43,21 @@ - + - - + - + - + @@ -67,49 +70,17 @@ - + - - + - - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - + @@ -124,91 +95,147 @@ - + - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + - - + + - - + - + @@ -248,7 +275,7 @@ - + @@ -265,7 +292,7 @@ - + @@ -284,21 +311,20 @@ - - + + - - + - + - + - + @@ -307,17 +333,17 @@ - + - + - + @@ -326,7 +352,7 @@ - + @@ -358,7 +384,7 @@ - - + @@ -397,7 +423,7 @@ - + @@ -430,7 +456,7 @@ - + @@ -448,7 +474,7 @@ - + @@ -474,10 +500,10 @@ - + - + @@ -597,11 +623,11 @@ - - - - - + + + - + @@ -775,7 +800,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/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.m b/Cocoa/GBImageView.m index 973625e..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; @@ -93,7 +93,7 @@ - (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 069db10..0000000 --- a/Cocoa/GBJoystickListener.h +++ /dev/null @@ -1,9 +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; -- (void) joystick:(NSString *)joystick_name hat: (unsigned)hat changedState: (int8_t) 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 ecf0311..31eebde 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; @@ -184,6 +238,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 +256,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 +300,6 @@ [self.skipButton setEnabled:YES]; joystick_being_configured = nil; [self advanceConfigurationStateMachine]; - last_axis = -1; } - (IBAction) skipButton:(id)sender @@ -223,11 +310,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,112 +324,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) joystick:(NSString *)joystick_name hat: (unsigned)hat changedState: (int8_t) state +- (void)setAnalogControlsCheckbox:(NSButton *)analogControlsCheckbox { - /* Hats are always mapped to the D-pad, ignore them on non-Dpad keys and skip the D-pad configuration if used*/ - if (!state) return; - if (joystick_configuration_state == -1) return; - if (joystick_configuration_state > GBDown) return; - if (!joystick_being_configured) { - joystick_being_configured = joystick_name; - } - else if (![joystick_being_configured isEqualToString:joystick_name]) { - 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]; - } - - for (joystick_configuration_state = 0;; joystick_configuration_state++) { - [mapping removeObjectForKey:GBButtonNames[joystick_configuration_state]]; - if (joystick_configuration_state == GBDown) break; - } - - all_mappings[joystick_name] = mapping; - [[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"]; - [self refreshJoypadMenu:nil]; - [self advanceConfigurationStateMachine]; + _analogControlsCheckbox = analogControlsCheckbox; + [_analogControlsCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]]; } - (NSButton *)aspectRatioCheckbox @@ -361,10 +431,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]; } @@ -483,21 +556,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]; @@ -505,18 +604,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.m b/Cocoa/GBTerminalTextFieldCell.m index 47a3a35..e95e785 100644 --- a/Cocoa/GBTerminalTextFieldCell.m +++ b/Cocoa/GBTerminalTextFieldCell.m @@ -173,7 +173,8 @@ [super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag]; } -- (BOOL)resignFirstResponder { +- (BOOL)resignFirstResponder +{ reverse_search_mode = false; return [super resignFirstResponder]; } 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 5a851f3..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 -= 1.0/16; - 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 += 1.0/16; - 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,123 +270,99 @@ } } -- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state +- (void)setRumble:(double)amp { - 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]; - - 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; - } - } - } - } + [lastController setRumbleAmplitude:amp]; } -- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value +- (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis { - unsigned player_count = GB_get_player_count(_gb); + if (![self.window isMainWindow]) return; - 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); - } - } + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + if (!mapping) { + mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; } -} - -- (void) joystick:(NSString *)joystick_name hat: (unsigned)hat changedState: (int8_t) state -{ - unsigned player_count = GB_get_player_count(_gb); - UpdateSystemActivity(UsrActivity); + 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); + + 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; } - assert(state + 1 < 9); - /* - N NE E SE S SW W NW */ - GB_set_key_state_for_player(_gb, GB_KEY_UP, player, (bool []){0, 1, 1, 0, 0, 0, 0, 0, 1}[state + 1]); - GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, (bool []){0, 0, 1, 1, 1, 0, 0, 0, 0}[state + 1]); - GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, (bool []){0, 0, 0, 0, 1, 1, 1, 0, 0}[state + 1]); - GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, (bool []){0, 0, 0, 0, 0, 0, 1, 1, 1}[state + 1]); + 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]; + } + + 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; + } } } 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 dd801cb..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-2019 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/License.html b/Cocoa/License.html index 49851fd..b21cf8d 100644 --- a/Cocoa/License.html +++ b/Cocoa/License.html @@ -30,7 +30,7 @@

SameBoy

MIT License

-

Copyright © 2015-2019 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/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 8278ee1..aa4a87d 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 @@ - + @@ -301,7 +396,7 @@ - + @@ -339,16 +434,16 @@ - + - + - - + + @@ -359,7 +454,7 @@ - + @@ -369,22 +464,11 @@ - + - - + @@ -392,8 +476,17 @@ + + + + + + + + + - + @@ -441,28 +534,28 @@ -