diff --git a/bsnes/gb/.gitattributes b/bsnes/gb/.gitattributes new file mode 100644 index 00000000..427cb285 --- /dev/null +++ b/bsnes/gb/.gitattributes @@ -0,0 +1,6 @@ +HexFiend/* linguist-vendored +*.inc linguist-language=C +Core/*.h linguist-language=C +SDL/*.h linguist-language=C +Windows/*.h linguist-language=C +Cocoa/*.h linguist-language=Objective-C diff --git a/bsnes/gb/.github/actions/LICENSE b/bsnes/gb/.github/actions/LICENSE new file mode 100644 index 00000000..8c295a2b --- /dev/null +++ b/bsnes/gb/.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/bsnes/gb/.github/actions/cgb-acid2.gbc b/bsnes/gb/.github/actions/cgb-acid2.gbc new file mode 100644 index 00000000..5f71bd36 Binary files /dev/null and b/bsnes/gb/.github/actions/cgb-acid2.gbc differ diff --git a/bsnes/gb/.github/actions/cgb_sound.gb b/bsnes/gb/.github/actions/cgb_sound.gb new file mode 100644 index 00000000..dc50471b Binary files /dev/null and b/bsnes/gb/.github/actions/cgb_sound.gb differ diff --git a/bsnes/gb/.github/actions/dmg-acid2.gb b/bsnes/gb/.github/actions/dmg-acid2.gb new file mode 100644 index 00000000..a25ef948 Binary files /dev/null and b/bsnes/gb/.github/actions/dmg-acid2.gb differ diff --git a/bsnes/gb/.github/actions/dmg_sound-2.gb b/bsnes/gb/.github/actions/dmg_sound-2.gb new file mode 100755 index 00000000..52dcf70b Binary files /dev/null and b/bsnes/gb/.github/actions/dmg_sound-2.gb differ diff --git a/bsnes/gb/.github/actions/install_deps.sh b/bsnes/gb/.github/actions/install_deps.sh new file mode 100755 index 00000000..1c9749ef --- /dev/null +++ b/bsnes/gb/.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/bsnes/gb/.github/actions/oam_bug-2.gb b/bsnes/gb/.github/actions/oam_bug-2.gb new file mode 100755 index 00000000..a3f55af1 Binary files /dev/null and b/bsnes/gb/.github/actions/oam_bug-2.gb differ diff --git a/bsnes/gb/.github/actions/sanity_tests.sh b/bsnes/gb/.github/actions/sanity_tests.sh new file mode 100755 index 00000000..8984b264 --- /dev/null +++ b/bsnes/gb/.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/bsnes/gb/.github/workflows/sanity.yml b/bsnes/gb/.github/workflows/sanity.yml new file mode 100644 index 00000000..f460931b --- /dev/null +++ b/bsnes/gb/.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/bsnes/gb/.gitignore b/bsnes/gb/.gitignore new file mode 100644 index 00000000..c795b054 --- /dev/null +++ b/bsnes/gb/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/bsnes/gb/BootROMs/SameBoyLogo.png b/bsnes/gb/BootROMs/SameBoyLogo.png new file mode 100644 index 00000000..c7cfc087 Binary files /dev/null and b/bsnes/gb/BootROMs/SameBoyLogo.png differ diff --git a/bsnes/gb/BootROMs/agb_boot.asm b/bsnes/gb/BootROMs/agb_boot.asm new file mode 100644 index 00000000..95a2c783 --- /dev/null +++ b/bsnes/gb/BootROMs/agb_boot.asm @@ -0,0 +1,2 @@ +AGB EQU 1 +include "cgb_boot.asm" \ No newline at end of file diff --git a/bsnes/gb/BootROMs/cgb_boot.asm b/bsnes/gb/BootROMs/cgb_boot.asm new file mode 100644 index 00000000..1345915d --- /dev/null +++ b/bsnes/gb/BootROMs/cgb_boot.asm @@ -0,0 +1,1239 @@ +; SameBoy CGB bootstrap ROM +; Todo: use friendly names for HW registers instead of magic numbers +SECTION "BootCode", ROM0[$0] +Start: +; Init stack pointer + ld sp, $fffe + +; Clear memory VRAM + call ClearMemoryPage8000 + ld a, 2 + ld c, $70 + ld [c], a +; Clear RAM Bank 2 (Like the original boot ROM) + ld h, $D0 + call ClearMemoryPage + ld [c], a + +; Clear OAM + ld h, $fe + ld c, $a0 +.clearOAMLoop + ldi [hl], a + dec c + jr nz, .clearOAMLoop + +; 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 + ld a, $f3 + ldh [$12], a + ldh [$25], a + ld a, $77 + ldh [$24], a + ld hl, $FF30 + +; Init BG palette + ld a, $fc + ldh [$47], a + +; Load logo from ROM. +; A nibble represents a 4-pixels line, 2 bytes represent a 4x4 tile, scaled to 8x8. +; Tiles are ordered left to right, top to bottom. +; These tiles are not used, but are required for DMG compatibility. This is done +; by the original CGB Boot ROM as well. + ld de, $104 ; Logo start + ld hl, $8010 ; This is where we load the tiles in VRAM + +.loadLogoLoop + ld a, [de] ; Read 2 rows + ld b, a + call DoubleBitsAndWriteRowTwice + inc de + ld a, e + cp $34 ; End of logo + jr nz, .loadLogoLoop + call ReadTrademarkSymbol + +; Clear the second VRAM bank + ld a, 1 + ldh [$4F], a + call ClearMemoryPage8000 + call LoadTileset + + ld b, 3 +IF DEF(FAST) + xor a + ldh [$4F], a +ELSE +; Load Tilemap + ld hl, $98C2 + ld d, 3 + ld a, 8 + +.tilemapLoop + ld c, $10 + +.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 + sub $20 + sub $3 + jr nc, .notspecial + add $20 + call .write_with_palette + dec c +.notspecial + pop af + + add d ; d = 3 for SameBoy logo, d = 1 for Nintendo logo + dec c + jr nz, .tilemapRowLoop + sub 44 + push de + ld de, $10 + add hl, de + pop de + dec b + jr nz, .tilemapLoop + + dec d + jr z, .endTilemap + dec d + + ld a, $38 + 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 + + ; Expand Palettes + ld de, AnimationColors + ld c, 8 + ld hl, BgPalettes + xor a +.expandPalettesLoop: + cpl + ; One white + ld [hli], a + ld [hli], a + + ; Mixed with white + ld a, [de] + inc e + or $20 + ld b, a + + ld a, [de] + 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 + dec c + jr nz, .expandPalettesLoop + + call LoadPalettesFromHRAM + + ; Turn on LCD + ld a, $91 + ldh [$40], a + +IF !DEF(FAST) + call DoIntroAnimation + + ld a, 45 + ldh [WaitLoopCounter], a +; Wait ~0.75 seconds + ld b, a + call WaitBFrames + + ; Play first sound + ld a, $83 + call PlaySound + ld b, 5 + call WaitBFrames + ; Play second sound + ld a, $c1 + call PlaySound + +.waitLoop + call GetInputPaletteIndex + call WaitFrame + ld hl, WaitLoopCounter + dec [hl] + jr nz, .waitLoop +ELSE + ld a, $c1 + call PlaySound +ENDC + call Preboot +IF DEF(AGB) + ld b, 1 +ENDC + +; Will be filled with NOPs + +SECTION "BootGame", ROM0[$fe] +BootGame: + ldh [$50], a + +SECTION "MoreStuff", ROM0[$200] +; Game Palettes Data +TitleChecksums: + db $00 ; Default + db $88 ; ALLEY WAY + db $16 ; YAKUMAN + db $36 ; BASEBALL, (Game and Watch 2) + db $D1 ; TENNIS + db $DB ; TETRIS + db $F2 ; QIX + db $3C ; DR.MARIO + db $8C ; RADARMISSION + db $92 ; F1RACE + db $3D ; YOSSY NO TAMAGO + db $5C ; + db $58 ; X + db $C9 ; MARIOLAND2 + db $3E ; YOSSY NO COOKIE + db $70 ; ZELDA + db $1D ; + db $59 ; + db $69 ; TETRIS FLASH + db $19 ; DONKEY KONG + db $35 ; MARIO'S PICROSS + db $A8 ; + db $14 ; POKEMON RED, (GAMEBOYCAMERA G) + db $AA ; POKEMON GREEN + db $75 ; PICROSS 2 + db $95 ; YOSSY NO PANEPON + db $99 ; KIRAKIRA KIDS + db $34 ; GAMEBOY GALLERY + db $6F ; POCKETCAMERA + db $15 ; + db $FF ; BALLOON KID + db $97 ; KINGOFTHEZOO + db $4B ; DMG FOOTBALL + db $90 ; WORLD CUP + db $17 ; OTHELLO + db $10 ; SUPER RC PRO-AM + db $39 ; DYNABLASTER + db $F7 ; BOY AND BLOB GB2 + db $F6 ; MEGAMAN + db $A2 ; STAR WARS-NOA + db $49 ; + db $4E ; WAVERACE + db $43 | $80 ; + db $68 ; LOLO2 + db $E0 ; YOSHI'S COOKIE + db $8B ; MYSTIC QUEST + db $F0 ; + db $CE ; TOPRANKINGTENNIS + db $0C ; MANSELL + db $29 ; MEGAMAN3 + db $E8 ; SPACE INVADERS + db $B7 ; GAME&WATCH + db $86 ; DONKEYKONGLAND95 + db $9A ; ASTEROIDS/MISCMD + db $52 ; STREET FIGHTER 2 + db $01 ; DEFENDER/JOUST + db $9D ; KILLERINSTINCT95 + db $71 ; TETRIS BLAST + db $9C ; PINOCCHIO + db $BD ; + db $5D ; BA.TOSHINDEN + db $6D ; NETTOU KOF 95 + db $67 ; + db $3F ; TETRIS PLUS + db $6B ; DONKEYKONGLAND 3 +; For these games, the 4th letter is taken into account +FirstChecksumWithDuplicate: + ; Let's play hangman! + db $B3 ; ???[B]???????? + db $46 ; SUP[E]R MARIOLAND + db $28 ; GOL[F] + db $A5 ; SOL[A]RSTRIKER + db $C6 ; GBW[A]RS + db $D3 ; KAE[R]UNOTAMENI + db $27 ; ???[B]???????? + db $61 ; POK[E]MON BLUE + db $18 ; DON[K]EYKONGLAND + db $66 ; GAM[E]BOY GALLERY2 + db $6A ; DON[K]EYKONGLAND 2 + db $BF ; KID[ ]ICARUS + db $0D ; TET[R]IS2 + db $F4 ; ???[-]???????? + db $B3 ; MOG[U]RANYA + db $46 ; ???[R]???????? + db $28 ; GAL[A]GA&GALAXIAN + db $A5 ; BT2[R]AGNAROKWORLD + db $C6 ; KEN[ ]GRIFFEY JR + db $D3 ; ???[I]???????? + db $27 ; MAG[N]ETIC SOCCER + db $61 ; VEG[A]S STAKES + db $18 ; ???[I]???????? + db $66 ; MIL[L]I/CENTI/PEDE + db $6A ; MAR[I]O & YOSHI + db $BF ; SOC[C]ER + db $0D ; POK[E]BOM + db $F4 ; G&W[ ]GALLERY + db $B3 ; TET[R]IS ATTACK +ChecksumsEnd: + +PalettePerChecksum: +palette_index: MACRO ; palette, flags + db ((\1) * 3) | (\2) ; | $80 means game requires DMG boot tilemap +ENDM + palette_index 0, 0 ; Default Palette + palette_index 4, 0 ; ALLEY WAY + palette_index 5, 0 ; YAKUMAN + palette_index 35, 0 ; BASEBALL, (Game and Watch 2) + palette_index 34, 0 ; TENNIS + palette_index 3, 0 ; TETRIS + palette_index 31, 0 ; QIX + palette_index 15, 0 ; DR.MARIO + palette_index 10, 0 ; RADARMISSION + palette_index 5, 0 ; F1RACE + palette_index 19, 0 ; YOSSY NO TAMAGO + palette_index 36, 0 ; + palette_index 7, $80 ; X + palette_index 37, 0 ; MARIOLAND2 + palette_index 30, 0 ; YOSSY NO COOKIE + palette_index 44, 0 ; ZELDA + palette_index 21, 0 ; + palette_index 32, 0 ; + palette_index 31, 0 ; TETRIS FLASH + palette_index 20, 0 ; DONKEY KONG + palette_index 5, 0 ; MARIO'S PICROSS + palette_index 33, 0 ; + palette_index 13, 0 ; POKEMON RED, (GAMEBOYCAMERA G) + palette_index 14, 0 ; POKEMON GREEN + palette_index 5, 0 ; PICROSS 2 + palette_index 29, 0 ; YOSSY NO PANEPON + palette_index 5, 0 ; KIRAKIRA KIDS + palette_index 18, 0 ; GAMEBOY GALLERY + palette_index 9, 0 ; POCKETCAMERA + palette_index 3, 0 ; + palette_index 2, 0 ; BALLOON KID + palette_index 26, 0 ; KINGOFTHEZOO + palette_index 25, 0 ; DMG FOOTBALL + palette_index 25, 0 ; WORLD CUP + palette_index 41, 0 ; OTHELLO + palette_index 42, 0 ; SUPER RC PRO-AM + palette_index 26, 0 ; DYNABLASTER + palette_index 45, 0 ; BOY AND BLOB GB2 + palette_index 42, 0 ; MEGAMAN + palette_index 45, 0 ; STAR WARS-NOA + palette_index 36, 0 ; + palette_index 38, 0 ; WAVERACE + palette_index 26, 0 ; + palette_index 42, 0 ; LOLO2 + palette_index 30, 0 ; YOSHI'S COOKIE + palette_index 41, 0 ; MYSTIC QUEST + palette_index 34, 0 ; + palette_index 34, 0 ; TOPRANKINGTENNIS + palette_index 5, 0 ; MANSELL + palette_index 42, 0 ; MEGAMAN3 + palette_index 6, 0 ; SPACE INVADERS + palette_index 5, 0 ; GAME&WATCH + palette_index 33, 0 ; DONKEYKONGLAND95 + palette_index 25, 0 ; ASTEROIDS/MISCMD + palette_index 42, 0 ; STREET FIGHTER 2 + palette_index 42, 0 ; DEFENDER/JOUST + palette_index 40, 0 ; KILLERINSTINCT95 + palette_index 2, 0 ; TETRIS BLAST + palette_index 16, 0 ; PINOCCHIO + palette_index 25, 0 ; + palette_index 42, 0 ; BA.TOSHINDEN + palette_index 42, 0 ; NETTOU KOF 95 + palette_index 5, 0 ; + palette_index 0, 0 ; TETRIS PLUS + palette_index 39, 0 ; DONKEYKONGLAND 3 + palette_index 36, 0 ; + palette_index 22, 0 ; SUPER MARIOLAND + palette_index 25, 0 ; GOLF + palette_index 6, 0 ; SOLARSTRIKER + palette_index 32, 0 ; GBWARS + palette_index 12, 0 ; KAERUNOTAMENI + palette_index 36, 0 ; + palette_index 11, 0 ; POKEMON BLUE + palette_index 39, 0 ; DONKEYKONGLAND + palette_index 18, 0 ; GAMEBOY GALLERY2 + palette_index 39, 0 ; DONKEYKONGLAND 2 + palette_index 24, 0 ; KID ICARUS + palette_index 31, 0 ; TETRIS2 + palette_index 50, 0 ; + palette_index 17, 0 ; MOGURANYA + palette_index 46, 0 ; + palette_index 6, 0 ; GALAGA&GALAXIAN + palette_index 27, 0 ; BT2RAGNAROKWORLD + palette_index 0, 0 ; KEN GRIFFEY JR + palette_index 47, 0 ; + palette_index 41, 0 ; MAGNETIC SOCCER + palette_index 41, 0 ; VEGAS STAKES + palette_index 0, 0 ; + palette_index 0, 0 ; MILLI/CENTI/PEDE + palette_index 19, 0 ; MARIO & YOSHI + palette_index 34, 0 ; SOCCER + palette_index 23, 0 ; POKEBOM + palette_index 18, 0 ; G&W GALLERY + palette_index 29, 0 ; TETRIS ATTACK + +Dups4thLetterArray: + db "BEFAARBEKEK R-URAR INAILICE R" + +; We assume the last three arrays fit in the same $100 byte page! + +PaletteCombinations: +palette_comb: MACRO ; Obj0, Obj1, Bg + db (\1) * 8, (\2) * 8, (\3) *8 +ENDM +raw_palette_comb: MACRO ; Obj0, Obj1, Bg + db (\1) * 2, (\2) * 2, (\3) * 2 +ENDM + palette_comb 4, 4, 29 + palette_comb 18, 18, 18 + palette_comb 20, 20, 20 + palette_comb 24, 24, 24 + palette_comb 9, 9, 9 + palette_comb 0, 0, 0 + palette_comb 27, 27, 27 + palette_comb 5, 5, 5 + palette_comb 12, 12, 12 + palette_comb 26, 26, 26 + palette_comb 16, 8, 8 + palette_comb 4, 28, 28 + palette_comb 4, 2, 2 + palette_comb 3, 4, 4 + palette_comb 4, 29, 29 + palette_comb 28, 4, 28 + palette_comb 2, 17, 2 + palette_comb 16, 16, 8 + palette_comb 4, 4, 7 + palette_comb 4, 4, 18 + palette_comb 4, 4, 20 + palette_comb 19, 19, 9 + raw_palette_comb 4 * 4 - 1, 4 * 4 - 1, 11 * 4 + palette_comb 17, 17, 2 + palette_comb 4, 4, 2 + palette_comb 4, 4, 3 + palette_comb 28, 28, 0 + palette_comb 3, 3, 0 + palette_comb 0, 0, 1 + palette_comb 18, 22, 18 + palette_comb 20, 22, 20 + palette_comb 24, 22, 24 + palette_comb 16, 22, 8 + palette_comb 17, 4, 13 + raw_palette_comb 28 * 4 - 1, 0 * 4, 14 * 4 + raw_palette_comb 28 * 4 - 1, 4 * 4, 15 * 4 + palette_comb 19, 22, 9 + palette_comb 16, 28, 10 + palette_comb 4, 23, 28 + palette_comb 17, 22, 2 + palette_comb 4, 0, 2 + palette_comb 4, 28, 3 + palette_comb 28, 3, 0 + palette_comb 3, 28, 4 + palette_comb 21, 28, 4 + palette_comb 3, 28, 0 + palette_comb 25, 3, 28 + palette_comb 0, 28, 8 + palette_comb 4, 3, 28 + palette_comb 28, 3, 6 + palette_comb 4, 28, 29 + ; SameBoy "Exclusives" + palette_comb 30, 30, 30 ; CGA + palette_comb 31, 31, 31 ; DMG LCD + palette_comb 28, 4, 1 + palette_comb 0, 0, 2 + +Palettes: + dw $7FFF, $32BF, $00D0, $0000 + dw $639F, $4279, $15B0, $04CB + dw $7FFF, $6E31, $454A, $0000 + dw $7FFF, $1BEF, $0200, $0000 + dw $7FFF, $421F, $1CF2, $0000 + dw $7FFF, $5294, $294A, $0000 + dw $7FFF, $03FF, $012F, $0000 + dw $7FFF, $03EF, $01D6, $0000 + dw $7FFF, $42B5, $3DC8, $0000 + dw $7E74, $03FF, $0180, $0000 + dw $67FF, $77AC, $1A13, $2D6B + dw $7ED6, $4BFF, $2175, $0000 + dw $53FF, $4A5F, $7E52, $0000 + dw $4FFF, $7ED2, $3A4C, $1CE0 + dw $03ED, $7FFF, $255F, $0000 + dw $036A, $021F, $03FF, $7FFF + dw $7FFF, $01DF, $0112, $0000 + dw $231F, $035F, $00F2, $0009 + dw $7FFF, $03EA, $011F, $0000 + dw $299F, $001A, $000C, $0000 + dw $7FFF, $027F, $001F, $0000 + dw $7FFF, $03E0, $0206, $0120 + dw $7FFF, $7EEB, $001F, $7C00 + dw $7FFF, $3FFF, $7E00, $001F + dw $7FFF, $03FF, $001F, $0000 + dw $03FF, $001F, $000C, $0000 + dw $7FFF, $033F, $0193, $0000 + dw $0000, $4200, $037F, $7FFF + dw $7FFF, $7E8C, $7C00, $0000 + dw $7FFF, $1BEF, $6180, $0000 + ; SameBoy "Exclusives" + dw $7FFF, $7FEA, $7D5F, $0000 ; CGA 1 + dw $4778, $3290, $1D87, $0861 ; DMG LCD + +KeyCombinationPalettes: + db 1 * 3 ; Right + db 48 * 3 ; Left + db 5 * 3 ; Up + db 8 * 3 ; Down + db 0 * 3 ; Right + A + db 40 * 3 ; Left + A + db 43 * 3 ; Up + A + db 3 * 3 ; Down + A + db 6 * 3 ; Right + B + db 7 * 3 ; Left + B + db 28 * 3 ; Up + B + db 49 * 3 ; Down + B + ; SameBoy "Exclusives" + db 51 * 3 ; Right + A + B + db 52 * 3 ; Left + A + B + db 53 * 3 ; Up + A + B + db 54 * 3 ; Down + A + B + +TrademarkSymbol: + db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c + +SameBoyLogo: + incbin "SameBoyLogo.pb12" + + +AnimationColors: + dw $7FFF ; White + dw $774F ; Cyan + dw $22C7 ; Green + dw $039F ; Yellow + dw $017D ; Orange + dw $241D ; Red + dw $6D38 ; Purple + dw $7102 ; Blue +AnimationColorsEnd: + +; Helper Functions +DoubleBitsAndWriteRowTwice: + call .twice +.twice +; Double the most significant 4 bits, b is shifted by 4 + ld a, 4 + ld c, 0 +.doubleCurrentBit + sla b + push af + rl c + pop af + rl c + dec a + jr nz, .doubleCurrentBit + ld a, c +; Write as two rows + ldi [hl], a + inc hl + ldi [hl], a + inc hl + ret + +WaitFrame: + push hl + ld hl, $FF0F + res 0, [hl] +.wait + bit 0, [hl] + jr z, .wait + pop hl + ret + +WaitBFrames: + call GetInputPaletteIndex + call WaitFrame + dec b + jr nz, WaitBFrames + ret + +PlaySound: + ldh [$13], a + ld a, $87 + ldh [$14], a + ret + +ClearMemoryPage8000: + ld hl, $8000 +; Clear from HL to HL | 0x2000 +ClearMemoryPage: + xor a + ldi [hl], a + bit 5, h + jr z, ClearMemoryPage + ret + +ReadTwoTileLines: + call ReadTileLine +; c = $f0 for even lines, $f for odd lines. +ReadTileLine: + ld a, [de] + and c + ld b, a + inc e + inc e + ld a, [de] + dec e + dec e + and c + swap a + or b + bit 0, c + jr z, .dontSwap + swap a +.dontSwap + inc hl + ldi [hl], a + swap c + ret + + +ReadCGBLogoHalfTile: + call .do_twice +.do_twice + call ReadTwoTileLines + inc e + 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: + ld hl, SameBoyLogo + ld de, SameBoyLogo_dst - 1 + ld c, SameBoyLogo_length +.refill + ; Register map for PB12 decompression + ; HL: source address in boot ROM + ; DE: destination address in VRAM + ; A: Current literal value + ; B: Repeat bits, terminated by 1000... + ; Source address in HL lets the repeat bits go straight to B, + ; bypassing A and avoiding spilling registers to the stack. + ld b, [hl] + dec b + jr z, .sameboyLogoEnd + inc b + inc hl + + ; Shift a 1 into lower bit of shift value. Once this bit + ; reaches the carry, B becomes 0 and the byte is over + scf + rl b + +.loop + ; If not a repeat, load a literal byte + jr c, .simple_repeat + sla b + jr c, .shifty_repeat + ld a, [hli] + jr .got_byte +.shifty_repeat + sla b + jr nz, .no_refill_during_shift + ld b, [hl] ; see above. Also, no, factoring it out into a callable + inc hl ; routine doesn't save bytes, even with conditional calls + scf + rl b +.no_refill_during_shift + ld c, a + jr nc, .shift_left + srl a + db $fe ; eat the add a with cp d8 +.shift_left + add a + sla b + jr c, .go_and + or c + db $fe ; eat the and c with cp d8 +.go_and + and c + jr .got_byte +.simple_repeat + sla b + jr c, .got_byte + ; far repeat + dec de + ld a, [de] + inc de +.got_byte + inc de + ld [de], a + sla b + jr nz, .loop + jr .refill + +; End PB12 decoding. The rest uses HL as the destination +.sameboyLogoEnd + ld h, d + ld l, $80 + +; Copy (unresized) ROM logo + ld de, $104 +.CGBROMLogoLoop + ld c, $f0 + call ReadCGBLogoHalfTile + add a, 22 + ld e, a + call ReadCGBLogoHalfTile + sub a, 22 + ld e, a + cp $1c + jr nz, .CGBROMLogoLoop + inc hl + ; fallthrough +ReadTrademarkSymbol: + ld de, TrademarkSymbol + ld c,$08 +.loadTrademarkSymbolLoop: + ld a,[de] + inc de + ldi [hl],a + inc hl + dec c + jr nz, .loadTrademarkSymbolLoop + ret + +DoIntroAnimation: + ; Animate the intro + ld a, 1 + ldh [$4F], a + ld d, 26 +.animationLoop + ld b, 2 + call WaitBFrames + ld hl, $98C0 + ld c, 3 ; Row count +.loop + ld a, [hl] + cp $F ; Already blue + jr z, .nextTile + inc [hl] + and $7 + jr z, .nextLine ; Changed a white tile, go to next line +.nextTile + inc hl + jr .loop +.nextLine + ld a, l + or $1F + ld l, a + inc hl + dec c + jr nz, .loop + dec d + jr nz, .animationLoop + ret + +Preboot: +IF !DEF(FAST) + ld b, 32 ; 32 times to fade +.fadeLoop + ld c, 32 ; 32 colors to fade + ld hl, BgPalettes +.frameLoop + push bc + + ; Brighten Color + ld a, [hli] + ld e, a + ld a, [hld] + ld d, a + ; RGB(1,1,1) + ld bc, $421 + + ; Is blue maxed? + ld a, e + and $1F + cp $1F + jr nz, .blueNotMaxed + dec c +.blueNotMaxed + + ; Is green maxed? + ld a, e + cp $E0 + jr c, .greenNotMaxed + ld a, d + and $3 + cp $3 + jr nz, .greenNotMaxed + res 5, c +.greenNotMaxed + + ; Is red maxed? + ld a, d + and $7C + cp $7C + jr nz, .redNotMaxed + res 2, b +.redNotMaxed + + ; add de, bc + ; ld [hli], de + ld a, e + add c + ld [hli], a + ld a, d + adc b + ld [hli], a + pop bc + + dec c + jr nz, .frameLoop + + call WaitFrame + call LoadPalettesFromHRAM + call WaitFrame + dec b + jr nz, .fadeLoop +ENDC + ld a, 1 + call ClearVRAMViaHDMA + call _ClearVRAMViaHDMA + call ClearVRAMViaHDMA ; A = $40, so it's bank 0 + ld a, $ff + ldh [$00], a + + ; Final values for CGB mode + ld d, a + ld e, c + ld l, $0d + + ld a, [$143] + bit 7, a + call z, EmulateDMG + bit 7, a + + ldh [$4C], a + ldh a, [TitleChecksum] + ld b, a + + jr z, .skipDMGForCGBCheck + ldh a, [InputPalette] + and a + jr nz, .emulateDMGForCGBGame +.skipDMGForCGBCheck +IF DEF(AGB) + ; Set registers to match the original AGB-CGB boot + ; AF = $1100, C = 0 + xor a + ld c, a + add a, $11 + ld h, c + ; B is set to 1 after ret +ELSE + ; Set registers to match the original CGB boot + ; AF = $1180, C = 0 + xor a + ld c, a + ld a, $11 + ld h, c + ; B is set to the title checksum +ENDC + ret + +.emulateDMGForCGBGame + call EmulateDMG + ldh [$4C], a + 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 + call GetPaletteIndex + bit 7, a + call nz, LoadDMGTilemap + and $7F + ld b, a + ldh a, [InputPalette] + and a + jr z, .nothingDown + call GetKeyComboPalette + jr .paletteFromKeys +.nothingDown + ld a, b +.paletteFromKeys + call WaitFrame + call LoadPalettesFromIndex + ld a, 4 + ; Set the final values for DMG mode + ld de, 8 + ld l, $7c + ret + +GetPaletteIndex: + ld hl, $14B + ld a, [hl] ; Old Licensee + cp $33 + jr z, .newLicensee + dec a ; 1 = Nintendo + jr nz, .notNintendo + jr .doChecksum +.newLicensee + ld l, $44 + ld a, [hli] + cp "0" + jr nz, .notNintendo + ld a, [hl] + cp "1" + jr nz, .notNintendo + +.doChecksum + ld l, $34 + ld c, $10 + xor a + +.checksumLoop + add [hl] + inc l + dec c + jr nz, .checksumLoop + ld b, a + + ; c = 0 + ld hl, TitleChecksums + +.searchLoop + ld a, l + sub LOW(ChecksumsEnd) ; use sub to zero out a + ret z + ld a, [hli] + cp b + jr nz, .searchLoop + + ; We might have a match, Do duplicate/4th letter check + ld a, l + sub FirstChecksumWithDuplicate - TitleChecksums + jr c, .match ; Does not have a duplicate, must be a match! + ; Has a duplicate; check 4th letter + push hl + ld a, l + add Dups4thLetterArray - FirstChecksumWithDuplicate - 1 ; -1 since hl was incremented + ld l, a + ld a, [hl] + pop hl + ld c, a + ld a, [$134 + 3] ; Get 4th letter + cp c + jr nz, .searchLoop ; Not a match, continue + +.match + ld a, l + add PalettePerChecksum - TitleChecksums - 1; -1 since hl was incremented + ld l, a + ld a, b + ldh [TitleChecksum], a + ld a, [hl] + ret + +.notNintendo + xor a + ret + +; 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 +.loadObjPalette + ld a, [hli] + push hl + ld hl, Palettes + ; b is already 0 + ld c, a + add hl, bc + ld d, 8 + ld c, $6A + call LoadPalettes + pop hl + bit 3, e + jr nz, .loadBGPalette + ld e, 8 + jr .loadObjPalette +.loadBGPalette + ;BG Palette + ld c, [hl] + ; b is already 0 + ld hl, Palettes + add hl, bc + ld d, 8 + jr LoadBGPalettes + +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 [c], a + dec d + jr nz, .loop + ret + +ClearVRAMViaHDMA: + 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 + ldh a, [$00] + cpl + and $F + ret z ; No direction keys pressed, no palette + + ld l, 0 +.directionLoop + inc l + rra + jr nc, .directionLoop + + ; c = 1: Right, 2: Left, 3: Up, 4: Down + + ld a, $10 ; Select buttons + ldh [$00], a + ldh a, [$00] + cpl + rla + rla + and $C + add l + ld l, a + ldh a, [InputPalette] + cp l + ret z ; No change, don't load + ld a, l + ldh [InputPalette], a + ; Slide into change Animation Palette + +ChangeAnimationPalette: + push bc + push de + call GetKeyComboPalette + call GetPaletteCombo + inc l + inc l + ld c, [hl] + ld hl, Palettes + 1 + add hl, bc + ld a, [hld] + cp $7F ; Is white color? + jr nz, .isWhite + inc hl + inc hl +.isWhite + push af + ld a, [hli] + + push hl + ld hl, BgPalettes ; First color, all palettes + call ReplaceColorInAllPalettes + ld l, LOW(BgPalettes + 2) ; Second color, all palettes + call ReplaceColorInAllPalettes + pop hl + ldh [BgPalettes + 6], a ; Fourth color, first palette + + ld a, [hli] + push hl + 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 + 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] + 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 + 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, [hli] + ldh [BgPalettes + 5], a ; Third color, first palette + + + call WaitFrame + call LoadPalettesFromHRAM + ; Delay the wait loop while the user is selecting a palette + ld a, 45 + ldh [WaitLoopCounter], a + pop de + pop bc + ret + +ReplaceColorInAllPalettes: + ld de, 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 [$9910], a ; ... put in the superscript position + ld hl,$992f ; Bottom right corner of the logo + ld c,$c ; Tiles in a logo row +.tilemapLoop + dec a + jr z, .tilemapDone + ldd [hl], a + dec c + jr nz, .tilemapLoop + ld l, $0f ; Jump to top row + jr .tilemapLoop +.tilemapDone + pop af + ret + +HDMAData: + db $88, $00, $98, $A0, $12 + db $88, $00, $80, $00, $40 + +BootEnd: +IF BootEnd > $900 + FAIL "BootROM overflowed: {BootEnd}" +ENDC + +SECTION "HRAM", HRAM[$FF80] +TitleChecksum: + ds 1 +BgPalettes: + ds 8 * 4 * 2 +InputPalette: + ds 1 +WaitLoopCounter: + ds 1 diff --git a/bsnes/gb/BootROMs/cgb_boot_fast.asm b/bsnes/gb/BootROMs/cgb_boot_fast.asm new file mode 100644 index 00000000..cddb4750 --- /dev/null +++ b/bsnes/gb/BootROMs/cgb_boot_fast.asm @@ -0,0 +1,2 @@ +FAST EQU 1 +include "cgb_boot.asm" \ No newline at end of file diff --git a/bsnes/gb/BootROMs/dmg_boot.asm b/bsnes/gb/BootROMs/dmg_boot.asm new file mode 100644 index 00000000..97a12e7c --- /dev/null +++ b/bsnes/gb/BootROMs/dmg_boot.asm @@ -0,0 +1,177 @@ +; SameBoy DMG bootstrap ROM +; Todo: use friendly names for HW registers instead of magic numbers +SECTION "BootCode", ROM0[$0] +Start: +; Init stack pointer + ld sp, $fffe + +; Clear memory VRAM + ld hl, $8000 + +.clearVRAMLoop + ldi [hl], a + bit 5, h + jr z, .clearVRAMLoop + +; Init Audio + ld a, $80 + ldh [$26], a + ldh [$11], a + ld a, $f3 + ldh [$12], a + ldh [$25], a + ld a, $77 + ldh [$24], a + +; Init BG palette + ld a, $54 + ldh [$47], a + +; Load logo from ROM. +; A nibble represents a 4-pixels line, 2 bytes represent a 4x4 tile, scaled to 8x8. +; Tiles are ordered left to right, top to bottom. + ld de, $104 ; Logo start + ld hl, $8010 ; This is where we load the tiles in VRAM + +.loadLogoLoop + ld a, [de] ; Read 2 rows + ld b, a + call DoubleBitsAndWriteRow + call DoubleBitsAndWriteRow + inc de + ld a, e + xor $34 ; End of logo + jr nz, .loadLogoLoop + +; Load trademark symbol + ld de, TrademarkSymbol + ld c,$08 +.loadTrademarkSymbolLoop: + ld a,[de] + inc de + ldi [hl],a + inc hl + dec c + jr nz, .loadTrademarkSymbolLoop + +; Set up tilemap + 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 +.tilemapLoop + dec a + jr z, .tilemapDone + ldd [hl], a + dec c + jr nz, .tilemapLoop + ld l,$0f ; Jump to top row + jr .tilemapLoop +.tilemapDone + + ld a, 30 + ldh [$ff42], a + + ; Turn on LCD + ld a, $91 + ldh [$40], a + + 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 + ld b, 5 + call WaitBFrames + ; Play second sound + ld a, $c1 + call PlaySound + + + +; Wait ~1 second + ld b, 60 + call WaitBFrames + +; Set registers to match the original DMG boot + ld hl, $01B0 + push hl + pop af + ld hl, $014D + ld bc, $0013 + ld de, $00D8 + +; Boot the game + jp BootGame + + +DoubleBitsAndWriteRow: +; Double the most significant 4 bits, b is shifted by 4 + ld a, 4 + ld c, 0 +.doubleCurrentBit + sla b + push af + rl c + pop af + rl c + dec a + jr nz, .doubleCurrentBit + ld a, c +; Write as two rows + ldi [hl], a + inc hl + ldi [hl], a + inc hl + ret + +WaitFrame: + push hl + ld hl, $FF0F + res 0, [hl] +.wait + bit 0, [hl] + jr z, .wait + pop hl + ret + +WaitBFrames: + call WaitFrame + dec b + jr nz, WaitBFrames + ret + +PlaySound: + ldh [$13], a + ld a, $87 + ldh [$14], a + ret + + +TrademarkSymbol: +db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c + +SECTION "BootGame", ROM0[$fe] +BootGame: + ldh [$50], a \ No newline at end of file diff --git a/bsnes/gb/BootROMs/pb12.c b/bsnes/gb/BootROMs/pb12.c new file mode 100644 index 00000000..cfedf6bb --- /dev/null +++ b/bsnes/gb/BootROMs/pb12.c @@ -0,0 +1,102 @@ +#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); +} + +void write_all(int fd, const void *buf, size_t count) { + while (count) { + ssize_t written = write(fd, buf, count); + if (written < 0) { + fprintf(stderr, "write"); + exit(1); + } + count -= written; + buf += written; + } +} + +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_all(STDOUT_FILENO, &outctl, 1); + write_all(STDOUT_FILENO, literals, literals_size); + bits -= 8; + control &= (1 << bits) - 1; + literals_size = 0; + } + } + uint8_t end_byte = 1; + write_all(STDOUT_FILENO, &end_byte, 1); + + return 0; +} diff --git a/bsnes/gb/BootROMs/sgb2_boot.asm b/bsnes/gb/BootROMs/sgb2_boot.asm new file mode 100644 index 00000000..1c3d8584 --- /dev/null +++ b/bsnes/gb/BootROMs/sgb2_boot.asm @@ -0,0 +1,2 @@ +SGB2 EQU 1 +include "sgb_boot.asm" \ No newline at end of file diff --git a/bsnes/gb/BootROMs/sgb_boot.asm b/bsnes/gb/BootROMs/sgb_boot.asm new file mode 100644 index 00000000..cdb9d774 --- /dev/null +++ b/bsnes/gb/BootROMs/sgb_boot.asm @@ -0,0 +1,213 @@ +; SameBoy SGB bootstrap ROM +; Todo: use friendly names for HW registers instead of magic numbers +SECTION "BootCode", ROM0[$0] +Start: +; Init stack pointer + ld sp, $fffe + +; Clear memory VRAM + ld hl, $8000 + +.clearVRAMLoop + ldi [hl], a + bit 5, h + jr z, .clearVRAMLoop + +; Init Audio + ld a, $80 + ldh [$26], a + ldh [$11], a + ld a, $f3 + ldh [$12], a + ldh [$25], a + ld a, $77 + ldh [$24], a + +; Init BG palette to white + ld a, $0 + ldh [$47], a + +; Load logo from ROM. +; A nibble represents a 4-pixels line, 2 bytes represent a 4x4 tile, scaled to 8x8. +; Tiles are ordered left to right, top to bottom. + ld de, $104 ; Logo start + ld hl, $8010 ; This is where we load the tiles in VRAM + +.loadLogoLoop + ld a, [de] ; Read 2 rows + ld b, a + call DoubleBitsAndWriteRow + call DoubleBitsAndWriteRow + inc de + ld a, e + xor $34 ; End of logo + jr nz, .loadLogoLoop + +; Load trademark symbol + ld de, TrademarkSymbol + ld c,$08 +.loadTrademarkSymbolLoop: + ld a,[de] + inc de + ldi [hl],a + inc hl + dec c + jr nz, .loadTrademarkSymbolLoop + +; Set up tilemap + 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 +.tilemapLoop + dec a + jr z, .tilemapDone + ldd [hl], a + dec c + jr nz, .tilemapLoop + ld l,$0f ; Jump to top row + jr .tilemapLoop +.tilemapDone + + ; Turn on LCD + ld a, $91 + ldh [$40], a + + ld a, $f1 ; Packet magic, increases by 2 for every packet + ldh [$80], a + ld hl, $104 ; Header start + + xor a + ld c, a ; JOYP + +.sendCommand + xor a + ld [c], a + ld a, $30 + ld [c], a + + ldh a, [$80] + call SendByte + push hl + ld b, $e + ld d, 0 + +.checksumLoop + call ReadHeaderByte + add d + ld d, a + dec b + jr nz, .checksumLoop + + ; Send checksum + call SendByte + pop hl + + ld b, $e +.sendLoop + call ReadHeaderByte + call SendByte + dec b + jr nz, .sendLoop + + ; Done bit + ld a, $20 + ld [c], a + ld a, $30 + ld [c], a + + ; Update command + ldh a, [$80] + add 2 + ldh [$80], a + + ld a, $58 + cp l + jr nz, .sendCommand + + ; Write to sound registers for DMG compatibility + ld c, $13 + ld a, $c1 + ld [c], a + inc c + ld a, 7 + ld [c], a + + ; Init BG palette + ld a, $fc + ldh [$47], a + +; Set registers to match the original SGB boot +IF DEF(SGB2) + ld a, $FF +ELSE + ld a, 1 +ENDC + ld hl, $c060 + +; Boot the game + jp BootGame + +ReadHeaderByte: + ld a, $4F + cp l + jr c, .zero + ld a, [hli] + ret +.zero: + inc hl + xor a + ret + +SendByte: + ld e, a + ld d, 8 +.loop + ld a, $10 + rr e + jr c, .zeroBit + add a ; 10 -> 20 +.zeroBit + ld [c], a + ld a, $30 + ld [c], a + dec d + ret z + jr .loop + +DoubleBitsAndWriteRow: +; Double the most significant 4 bits, b is shifted by 4 + ld a, 4 + ld c, 0 +.doubleCurrentBit + sla b + push af + rl c + pop af + rl c + dec a + jr nz, .doubleCurrentBit + ld a, c +; Write as two rows + ldi [hl], a + inc hl + ldi [hl], a + inc hl + ret + +WaitFrame: + push hl + ld hl, $FF0F + res 0, [hl] +.wait + bit 0, [hl] + jr z, .wait + pop hl + ret + +TrademarkSymbol: +db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c + +SECTION "BootGame", ROM0[$fe] +BootGame: + ldh [$50], a \ No newline at end of file diff --git a/bsnes/gb/CHANGES.md b/bsnes/gb/CHANGES.md new file mode 100644 index 00000000..b94aec4c --- /dev/null +++ b/bsnes/gb/CHANGES.md @@ -0,0 +1 @@ +See https://sameboy.github.io/changelog/ \ No newline at end of file diff --git a/bsnes/gb/CONTRIBUTING.md b/bsnes/gb/CONTRIBUTING.md new file mode 100644 index 00000000..94627d1a --- /dev/null +++ b/bsnes/gb/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# SameBoy Coding and Contribution Guidelines + +## Issues + +GitHub Issues are the most effective way to report a bug or request a feature in SameBoy. When reporting a bug, make sure you use the latest stable release, and make sure you mention the SameBoy frontend (Cocoa, SDL, Libretro) and operating system you're using. If you're using Linux/BSD/etc, or you build your own copy of SameBoy for another reason, give as much details as possible on your environment. + +If your bug involves a crash, please attach a crash log or a core dump. If you're using Linux/BSD/etc, or if you're using the Libretro core, please attach the `sameboy` binary (or `libretro_sameboy` library) in that case. + +If your bug is a regression, it'd be extremely helpful if you can report the the first affected version. You get extra credits if you use `git bisect` to point the exact breaking commit. + +If your bug is an emulation bug (Such as a failing test ROM), and you have access to a Game Boy you can test on, please confirm SameBoy is indeed behaving differently from hardware, and report both the emulated model and revision in SameBoy, and the hardware revision you're testing on. + +If your issue is a feature request, demonstrating use cases can help me better prioritize it. + +## Pull Requests + +To allow quicker integration into SameBoy's master branch, contributors are asked to follow SameBoy's style and coding guidelines. Keep in mind that despite the seemingly strict guidelines, all pull requests are welcome – not following the guidelines does not mean your pull request will not be accepted, but it will require manual tweaks from my side for integrating. + +### Languages and Compilers + +SameBoy's core, SDL frontend, Libretro frontend, and automatic tester (Folders `Core`, `SDL` & `OpenDialog`, `libretro`, and `Tester`; respectively) are all written in C11. The Cocoa frontend, SameBoy's fork of Hex Fiend, JoyKit and the Quick Look previewer (Folders `Cocoa`, `HexFiend`, `JoyKit` and `QuickLook`; respectively) are all written in ARC-enabled Objective-C. The SameBoot ROMs (Under `BootROMs`) are written in rgbds-flavor SM83 assembly, with build tools in C11. The shaders (inside `Shaders`) are written in a polyglot GLSL and Metal style, with a few GLSL- and Metal-specific sources. The build system uses standalone Make, in the GNU flavor. Avoid adding new languages (C++, Swift, Python, CMake...) to any of the existing sub-projects. + +SameBoy's main target compiler is Clang, but GCC is also supported when targeting Linux and Libretro. Other compilers (e.g. MSVC) are not supported, and unless there's a good reason, there's no need to go out of your way to add specific support for them. Extensions that are supported by both compilers (Such as `typeof`) may be used if it makes sense. It's OK if you can't test one of these compilers yourself; once you push a commit, the CI bot will let you know if you broke something. + +### Third Party Libraries and Tools + +Avoid adding new required dependencies; run-time and compile-time dependencies alike. Most importantly, avoid linking against GPL licensed libraries (LGPL libraries are fine), so SameBoy can retain its MIT license. + +### Spacing, Indentation and Formatting + +In all files and languages (Other than Makefiles when required), 4 spaces are used for indentation. Unix line endings (`\n`) are used exclusively, even in Windows-specific source files. (`\r` and `\t` shouldn't appear in any source file). Opening braces belong on the same line as their control flow directive, and on their own line when following a function prototype. The `else` keyword always starts on its own line. The `case` keyword is indented relative to its `switch` block, and the code inside a `case` is indented relative to its label. A control flow keyword should have a space between it and the following `(`, commas should follow a space, and operator (except `.` and `->`) should be surrounded by spaces. + +Control flow statements must use `{}`, with the exception of `if` statements that only contain a single `break`, `continue`, or trivial `return` statements. If `{}`s are omitted, the statement must be on the same line as the `if` condition. Functions that do not have any argument must be specified as `(void)`, as mandated by the C standard. The `sizeof` and `typeof` operators should be used as if they're functions (With `()`). `*`, when used to declare pointer types (including functions that return pointers), and when used to dereference a pointer, is attached to the right side (The variable name) – not to the left, and not with spaces on both sides. + +No strict limitations on a line's maximum width, but use your best judgement if you think a statement would benefit from an additional line break. + +Well formatted code example: + +``` +static void my_function(void) +{ + GB_something_t *thing = GB_function(&gb, GB_FLAG_ONE | GB_FLAG_TWO, sizeof(thing)); + if (GB_is_thing(thing)) return; + + switch (*thing) { + case GB_QUACK: + // Something + case GB_DUCK: + // Something else + } +} +``` + +Badly formatted code example: +``` +static void my_function(){ + GB_something_t* thing=GB_function(&gb , GB_FLAG_ONE|GB_FLAG_TWO , sizeof thing); + if( GB_is_thing ( thing ) ) + return; + + switch(* thing) + { + case GB_QUACK: + // Something + case GB_DUCK: + // Something else + } +} +``` + +### Other Coding Conventions + +The primitive types to be used in SameBoy are `unsigned` and `signed` (Without the `int` keyword), the `(u)int*_t` types, `char *` for UTF-8 strings, `double` for non-integer numbers, and `bool` for booleans (Including in Objective-C code, avoid `BOOL`). As long as it's not mandated by a 3rd-party API (e.g. `int` when using file descriptors), avoid using other primitive types. Use `const` whenever possible. + +Most C names should be `lower_case_snake_case`. Constants and macros use `UPPER_CASE_SNAKE_CASE`. Type definitions use a `_t` suffix. Type definitions, as well as non-static (exported) core symbols, should be prefixed with `GB_` (SameBoy's core is intended to be used as a library, so it shouldn't contaminate the global namespace without prefixes). Exported symbols that are only meant to be used by other parts of the core should still get the `GB_` prefix, but their header definition should be inside `#ifdef GB_INTERNAL`. + +For Objective-C naming conventions, use Apple's conventions (Some old Objective-C code mixes these with the C naming convention; new code should use Apple's convention exclusively). The name prefix for SameBoy classes and constants is `GB`. JoyKit's prefix is `JOY`, and Hex Fiend's prefix is `HF`. + +In all languages, prefer long, unambiguous names over short ambiguous ones. diff --git a/bsnes/gb/Cocoa/AppDelegate.h b/bsnes/gb/Cocoa/AppDelegate.h new file mode 100644 index 00000000..22e0c365 --- /dev/null +++ b/bsnes/gb/Cocoa/AppDelegate.h @@ -0,0 +1,15 @@ +#import + +@interface AppDelegate : NSObject + +@property IBOutlet NSWindow *preferencesWindow; +@property (strong) IBOutlet NSView *graphicsTab; +@property (strong) IBOutlet NSView *emulationTab; +@property (strong) IBOutlet NSView *audioTab; +@property (strong) IBOutlet NSView *controlsTab; +- (IBAction)showPreferences: (id) sender; +- (IBAction)toggleDeveloperMode:(id)sender; +- (IBAction)switchPreferencesTab:(id)sender; + +@end + diff --git a/bsnes/gb/Cocoa/AppDelegate.m b/bsnes/gb/Cocoa/AppDelegate.m new file mode 100644 index 00000000..133fab7f --- /dev/null +++ b/bsnes/gb/Cocoa/AppDelegate.m @@ -0,0 +1,112 @@ +#import "AppDelegate.h" +#include "GBButtons.h" +#include "GBView.h" +#include +#import +#import + +@implementation AppDelegate +{ + NSWindow *preferences_window; + NSArray *preferences_tabs; +} + +- (void) applicationDidFinishLaunching:(NSNotification *)notification +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + for (unsigned i = 0; i < GBButtonCount; i++) { + if ([[defaults objectForKey:button_to_preference_name(i, 0)] isKindOfClass:[NSString class]]) { + [defaults removeObjectForKey:button_to_preference_name(i, 0)]; + } + } + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"GBRight": @(kVK_RightArrow), + @"GBLeft": @(kVK_LeftArrow), + @"GBUp": @(kVK_UpArrow), + @"GBDown": @(kVK_DownArrow), + + @"GBA": @(kVK_ANSI_X), + @"GBB": @(kVK_ANSI_Z), + @"GBSelect": @(kVK_Delete), + @"GBStart": @(kVK_Return), + + @"GBTurbo": @(kVK_Space), + @"GBRewind": @(kVK_Tab), + @"GBSlow-Motion": @(kVK_Shift), + + @"GBFilter": @"NearestNeighbor", + @"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, + }]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; + } +} + +- (IBAction)toggleDeveloperMode:(id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:![defaults boolForKey:@"DeveloperMode"] forKey:@"DeveloperMode"]; +} + +- (IBAction)switchPreferencesTab:(id)sender +{ + for (NSView *view in preferences_tabs) { + [view removeFromSuperview]; + } + NSView *tab = preferences_tabs[[sender tag]]; + NSRect old = [_preferencesWindow frame]; + NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame]; + new.origin.x = old.origin.x; + new.origin.y = old.origin.y + (old.size.height - new.size.height); + [_preferencesWindow setFrame:new display:YES animate:_preferencesWindow.visible]; + [_preferencesWindow.contentView addSubview:tab]; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)anItem +{ + if ([anItem action] == @selector(toggleDeveloperMode:)) { + [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]]; + } + + return true; +} + +- (IBAction) showPreferences: (id) sender +{ + NSArray *objects; + if (!_preferencesWindow) { + [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects]; + NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject]; + _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier]; + preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab]; + [self switchPreferencesTab:first_toolbar_item]; + [_preferencesWindow center]; + } + [_preferencesWindow makeKeyAndOrderFront:self]; +} + +- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender +{ + [[NSDocumentController sharedDocumentController] openDocument:self]; + return YES; +} + +- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification +{ + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:YES]; +} +@end diff --git a/bsnes/gb/Cocoa/AppIcon.icns b/bsnes/gb/Cocoa/AppIcon.icns new file mode 100644 index 00000000..0fe80059 Binary files /dev/null and b/bsnes/gb/Cocoa/AppIcon.icns differ diff --git a/bsnes/gb/Cocoa/BigSurToolbar.h b/bsnes/gb/Cocoa/BigSurToolbar.h new file mode 100644 index 00000000..ea8b3700 --- /dev/null +++ b/bsnes/gb/Cocoa/BigSurToolbar.h @@ -0,0 +1,30 @@ +#import +#ifndef BigSurToolbar_h +#define BigSurToolbar_h + +/* Backport the toolbarStyle property to allow compilation with older SDKs*/ +#ifndef __MAC_10_16 +typedef NS_ENUM(NSInteger, NSWindowToolbarStyle) { + // The default value. The style will be determined by the window's given configuration + NSWindowToolbarStyleAutomatic, + // The toolbar will appear below the window title + NSWindowToolbarStyleExpanded, + // The toolbar will appear below the window title and the items in the toolbar will attempt to have equal widths when possible + NSWindowToolbarStylePreference, + // The window title will appear inline with the toolbar when visible + NSWindowToolbarStyleUnified, + // Same as NSWindowToolbarStyleUnified, but with reduced margins in the toolbar allowing more focus to be on the contents of the window + NSWindowToolbarStyleUnifiedCompact +} API_AVAILABLE(macos(11.0)); + +@interface NSWindow (toolbarStyle) +@property NSWindowToolbarStyle toolbarStyle API_AVAILABLE(macos(11.0)); +@end + +@interface NSImage (SFSymbols) ++ (instancetype)imageWithSystemSymbolName:(NSString *)symbolName accessibilityDescription:(NSString *)description API_AVAILABLE(macos(11.0)); +@end + +#endif + +#endif diff --git a/bsnes/gb/Cocoa/CPU.png b/bsnes/gb/Cocoa/CPU.png new file mode 100644 index 00000000..7f136213 Binary files /dev/null and b/bsnes/gb/Cocoa/CPU.png differ diff --git a/bsnes/gb/Cocoa/CPU@2x.png b/bsnes/gb/Cocoa/CPU@2x.png new file mode 100644 index 00000000..3c86883a Binary files /dev/null and b/bsnes/gb/Cocoa/CPU@2x.png differ diff --git a/bsnes/gb/Cocoa/Cartridge.icns b/bsnes/gb/Cocoa/Cartridge.icns new file mode 100644 index 00000000..6e0c78dd Binary files /dev/null and b/bsnes/gb/Cocoa/Cartridge.icns differ diff --git a/bsnes/gb/Cocoa/ColorCartridge.icns b/bsnes/gb/Cocoa/ColorCartridge.icns new file mode 100644 index 00000000..35fb2932 Binary files /dev/null and b/bsnes/gb/Cocoa/ColorCartridge.icns differ diff --git a/bsnes/gb/Cocoa/Display.png b/bsnes/gb/Cocoa/Display.png new file mode 100644 index 00000000..5753f558 Binary files /dev/null and b/bsnes/gb/Cocoa/Display.png differ diff --git a/bsnes/gb/Cocoa/Display@2x.png b/bsnes/gb/Cocoa/Display@2x.png new file mode 100644 index 00000000..6a71d221 Binary files /dev/null and b/bsnes/gb/Cocoa/Display@2x.png differ diff --git a/bsnes/gb/Cocoa/Document.h b/bsnes/gb/Cocoa/Document.h new file mode 100644 index 00000000..660d7bc2 --- /dev/null +++ b/bsnes/gb/Cocoa/Document.h @@ -0,0 +1,45 @@ +#import +#include "GBView.h" +#include "GBImageView.h" +#include "GBSplitView.h" + +@class GBCheatWindowController; + +@interface Document : NSDocument +@property (strong) IBOutlet GBView *view; +@property (strong) IBOutlet NSTextView *consoleOutput; +@property (strong) IBOutlet NSPanel *consoleWindow; +@property (strong) IBOutlet NSTextField *consoleInput; +@property (strong) IBOutlet NSWindow *mainWindow; +@property (strong) IBOutlet NSView *memoryView; +@property (strong) IBOutlet NSPanel *memoryWindow; +@property (readonly) GB_gameboy_t *gameboy; +@property (strong) IBOutlet NSTextField *memoryBankInput; +@property (strong) IBOutlet NSToolbarItem *memoryBankItem; +@property (strong) IBOutlet GBImageView *tilesetImageView; +@property (strong) IBOutlet NSPopUpButton *tilesetPaletteButton; +@property (strong) IBOutlet GBImageView *tilemapImageView; +@property (strong) IBOutlet NSPopUpButton *tilemapPaletteButton; +@property (strong) IBOutlet NSPopUpButton *tilemapMapButton; +@property (strong) IBOutlet NSPopUpButton *TilemapSetButton; +@property (strong) IBOutlet NSButton *gridButton; +@property (strong) IBOutlet NSTabView *vramTabView; +@property (strong) IBOutlet NSPanel *vramWindow; +@property (strong) IBOutlet NSTextField *vramStatusLabel; +@property (strong) IBOutlet NSTableView *paletteTableView; +@property (strong) IBOutlet NSTableView *spritesTableView; +@property (strong) IBOutlet NSPanel *printerFeedWindow; +@property (strong) IBOutlet NSImageView *feedImageView; +@property (strong) IBOutlet NSTextView *debuggerSideViewInput; +@property (strong) IBOutlet NSTextView *debuggerSideView; +@property (strong) IBOutlet GBSplitView *debuggerSplitView; +@property (strong) IBOutlet NSBox *debuggerVerticalLine; +@property (strong) IBOutlet NSPanel *cheatsWindow; +@property (strong) IBOutlet GBCheatWindowController *cheatWindowController; + +-(uint8_t) readMemory:(uint16_t) addr; +-(void) writeMemory:(uint16_t) addr value:(uint8_t)value; +-(void) performAtomicBlock: (void (^)())block; + +@end + diff --git a/bsnes/gb/Cocoa/Document.m b/bsnes/gb/Cocoa/Document.m new file mode 100644 index 00000000..653179d6 --- /dev/null +++ b/bsnes/gb/Cocoa/Document.m @@ -0,0 +1,1835 @@ +#include +#include +#include +#include "GBAudioClient.h" +#include "Document.h" +#include "AppDelegate.h" +#include "HexFiend/HexFiend.h" +#include "GBMemoryByteArray.h" +#include "GBWarningPopover.h" +#include "GBCheatWindowController.h" +#include "GBTerminalTextFieldCell.h" +#include "BigSurToolbar.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!!! */ + +enum model { + MODEL_NONE, + MODEL_DMG, + MODEL_CGB, + MODEL_AGB, + MODEL_SGB, +}; + +@interface Document () +{ + + NSMutableAttributedString *pending_console_output; + NSRecursiveLock *console_output_lock; + NSTimer *console_output_timer; + + bool fullScreen; + bool in_sync_input; + HFController *hex_controller; + + NSString *lastConsoleInput; + HFLineCountingRepresenter *lineRep; + + CVImageBufferRef cameraImage; + AVCaptureSession *cameraSession; + AVCaptureConnection *cameraConnection; + AVCaptureStillImageOutput *cameraOutput; + + GB_oam_info_t oamInfo[40]; + uint16_t oamCount; + uint8_t oamHeight; + bool oamUpdating; + + NSMutableData *currentPrinterImageData; + enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy} accessory; + + bool rom_warning_issued; + + NSMutableString *capturedOutput; + bool logToSideView; + bool shouldClearSideView; + enum model current_model; + + bool rewind; + bool modelsChanging; + + NSCondition *audioLock; + GB_sample_t *audioBuffer; + size_t audioBufferSize; + size_t audioBufferPosition; + size_t audioBufferNeeded; + + bool borderModeChanged; +} + +@property GBAudioClient *audioClient; +- (void) vblank; +- (void) log: (const char *) log withAttributes: (GB_log_attributes) attributes; +- (char *) getDebuggerInput; +- (char *) getAsyncDebuggerInput; +- (void) cameraRequestUpdate; +- (uint8_t) cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y; +- (void) printImage:(uint32_t *)image height:(unsigned) height + topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin + exposure:(unsigned) exposure; +- (void) gotNewSample:(GB_sample_t *)sample; +- (void) rumbleChanged:(double)amp; +- (void) loadBootROM:(GB_boot_rom_t)type; +@end + +static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self loadBootROM: type]; +} + +static void vblank(GB_gameboy_t *gb) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self vblank]; +} + +static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self log:string withAttributes: attributes]; +} + +static char *consoleInput(GB_gameboy_t *gb) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + return [self getDebuggerInput]; +} + +static char *asyncConsoleInput(GB_gameboy_t *gb) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + char *ret = [self getAsyncDebuggerInput]; + return ret; +} + +static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return (r << 0) | (g << 8) | (b << 16) | 0xFF000000; +} + +static void cameraRequestUpdate(GB_gameboy_t *gb) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self cameraRequestUpdate]; +} + +static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + return [self cameraGetPixelAtX:x andY:y]; +} + +static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, + uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; +} + +static void setWorkboyTime(GB_gameboy_t *gb, time_t t) +{ + [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"]; +} + +static time_t getWorkboyTime(GB_gameboy_t *gb) +{ + return time(NULL) - [[NSUserDefaults standardUserDefaults] integerForKey:@"GBWorkboyTimeOffset"]; +} + +static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self gotNewSample:sample]; +} + +static void rumbleCallback(GB_gameboy_t *gb, double amp) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self rumbleChanged:amp]; +} + +@implementation Document +{ + GB_gameboy_t gb; + volatile bool running; + volatile bool stopping; + NSConditionLock *has_debugger_input; + NSMutableArray *debugger_input_queue; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + has_debugger_input = [[NSConditionLock alloc] initWithCondition:0]; + debugger_input_queue = [[NSMutableArray alloc] init]; + console_output_lock = [[NSRecursiveLock alloc] init]; + audioLock = [[NSCondition alloc] init]; + } + return self; +} + +- (NSString *)bootROMPathForName:(NSString *)name +{ + NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"]; + if (url) { + NSString *path = [url path]; + path = [path stringByAppendingPathComponent:name]; + path = [path stringByAppendingPathExtension:@"bin"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + return path; + } + } + + return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"]; +} + +- (GB_model_t)internalModel +{ + switch (current_model) { + case MODEL_DMG: + return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"]; + + case MODEL_NONE: + case MODEL_CGB: + return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]; + + case MODEL_SGB: + return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]; + + case MODEL_AGB: + return GB_MODEL_AGB; + } +} + +- (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(), ^{ + self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0; + [self reloadVRAMData: nil]; + }); + } + if (self.view.isRewinding) { + rewind = true; + } +} + +- (void)gotNewSample:(GB_sample_t *)sample +{ + [audioLock lock]; + if (self.audioClient.isPlaying) { + if (audioBufferPosition == audioBufferSize) { + if (audioBufferSize >= 0x4000) { + audioBufferPosition = 0; + [audioLock unlock]; + return; + } + + if (audioBufferSize == 0) { + audioBufferSize = 512; + } + else { + audioBufferSize += audioBufferSize >> 2; + } + audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize); + } + audioBuffer[audioBufferPosition++] = *sample; + } + if (audioBufferPosition == audioBufferNeeded) { + [audioLock signal]; + audioBufferNeeded = 0; + } + [audioLock unlock]; +} + +- (void)rumbleChanged:(double)amp +{ + [_view setRumble:amp]; +} + +- (void) run +{ + running = true; + GB_set_pixels_output(&gb, self.view.pixels); + GB_set_sample_rate(&gb, 96000); + self.audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { + [audioLock lock]; + + if (audioBufferPosition < nFrames) { + audioBufferNeeded = nFrames; + [audioLock wait]; + } + + if (stopping) { + memset(buffer, 0, nFrames * sizeof(*buffer)); + [audioLock unlock]; + return; + } + + if (audioBufferPosition >= nFrames && audioBufferPosition < nFrames + 4800) { + memcpy(buffer, audioBuffer, nFrames * sizeof(*buffer)); + memmove(audioBuffer, audioBuffer + nFrames, (audioBufferPosition - nFrames) * sizeof(*buffer)); + audioBufferPosition = audioBufferPosition - nFrames; + } + else { + memcpy(buffer, audioBuffer + (audioBufferPosition - nFrames), nFrames * sizeof(*buffer)); + audioBufferPosition = 0; + } + [audioLock unlock]; + } andSampleRate:96000]; + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { + [self.audioClient start]; + } + NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; + + /* Clear pending alarms, don't play alarms while playing */ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; + for (NSUserNotification *notification in [center scheduledNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeScheduledNotification:notification]; + break; + } + } + + for (NSUserNotification *notification in [center deliveredNotifications]) { + if ([notification.identifier isEqualToString:self.fileName]) { + [center removeDeliveredNotification:notification]; + break; + } + } + } + + while (running) { + if (rewind) { + rewind = false; + GB_rewind_pop(&gb); + if (!GB_rewind_pop(&gb)) { + rewind = self.view.isRewinding; + } + } + else { + GB_run(&gb); + } + } + [hex_timer invalidate]; + [audioLock lock]; + memset(audioBuffer, 0, (audioBufferSize - audioBufferPosition) * sizeof(*audioBuffer)); + audioBufferPosition = audioBufferNeeded; + [audioLock signal]; + [audioLock unlock]; + [self.audioClient stop]; + self.audioClient = nil; + self.view.mouseHidingEnabled = NO; + GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); + GB_save_cheats(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"cht"] UTF8String]); + unsigned time_to_alarm = GB_time_to_alarm(&gb); + + if (time_to_alarm) { + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate]; + 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; +} + +- (void) start +{ + if (running) return; + self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0; + [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start]; +} + +- (void) stop +{ + if (!running) return; + GB_debugger_set_disabled(&gb, true); + if (GB_debugger_is_stopped(&gb)) { + [self interruptDebugInputRead]; + } + [audioLock lock]; + stopping = true; + [audioLock signal]; + [audioLock unlock]; + running = false; + while (stopping) { + [audioLock lock]; + [audioLock signal]; + [audioLock unlock]; + } + GB_debugger_set_disabled(&gb, false); +} + +- (void) loadBootROM: (GB_boot_rom_t)type +{ + 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 +{ + [self stop]; + size_t old_width = GB_get_screen_width(&gb); + + if ([sender tag] != MODEL_NONE) { + current_model = (enum model)[sender tag]; + } + + if (!modelsChanging && [sender tag] == MODEL_NONE) { + GB_reset(&gb); + } + else { + GB_switch_model_and_reset(&gb, [self internalModel]); + } + + if (old_width != GB_get_screen_width(&gb)) { + [self.view screenSizeChanged]; + } + + [self updateMinSize]; + + if ([sender tag] != 0) { + /* User explictly selected a model, save the preference */ + [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_DMG forKey:@"EmulateDMG"]; + [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_SGB forKey:@"EmulateSGB"]; + [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_AGB forKey:@"EmulateAGB"]; + } + + /* Reload the ROM, SAV and SYM files */ + [self loadROM]; + + [self start]; + + if (hex_controller) { + /* Verify bank sanity, especially when switching models. */ + [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:0]; + [self hexUpdateBank:self.memoryBankInput ignoreErrors:true]; + } +} + +- (IBAction)togglePause:(id)sender +{ + if (running) { + [self stop]; + } + else { + [self start]; + } +} + +- (void)dealloc +{ + [cameraSession stopRunning]; + self.view.gb = NULL; + GB_free(&gb); + if (cameraImage) { + CVBufferRelease(cameraImage); + } + if (audioBuffer) { + free(audioBuffer); + } +} + +- (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]]) { + [(NSOpenPanel *)window cancel:self]; + } + } + + NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init]; + [paragraph_style setLineSpacing:2]; + + self.debuggerSideViewInput.font = [NSFont userFixedPitchFontOfSize:12]; + self.debuggerSideViewInput.textColor = [NSColor whiteColor]; + self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style; + [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"]; + ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &gb; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateSideView) + name:NSTextDidChangeNotification + object:self.debuggerSideViewInput]; + + self.consoleOutput.textContainerInset = NSMakeSize(4, 4); + [self.view becomeFirstResponder]; + 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); + window_frame.size.height = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowHeight"], + window_frame.size.height); + [self.mainWindow setFrame:window_frame display:YES]; + self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised; + + + + self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[self.fileURL path] lastPathComponent]]; + self.debuggerSplitView.dividerColor = [NSColor clearColor]; + if (@available(macOS 11.0, *)) { + self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded; + self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact; + [self.printerFeedWindow.toolbar removeItemAtIndex:1]; + self.printerFeedWindow.toolbar.items.firstObject.image = + [NSImage imageWithSystemSymbolName:@"square.and.arrow.down" + accessibilityDescription:@"Save"]; + self.printerFeedWindow.toolbar.items.lastObject.image = + [NSImage imageWithSystemSymbolName:@"printer" + accessibilityDescription:@"Print"]; + } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateHighpassFilter) + name:@"GBHighpassFilterChanged" + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateColorCorrectionMode) + 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" + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(dmgModelChanged) + name:@"GBDMGModelChanged" + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(sgbModelChanged) + name:@"GBSGBModelChanged" + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(cgbModelChanged) + name:@"GBCGBModelChanged" + object:nil]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) { + current_model = MODEL_DMG; + } + else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateSGB"]) { + current_model = MODEL_SGB; + } + else { + current_model = [[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateAGB"]? MODEL_AGB : MODEL_CGB; + } + + [self initCommon]; + self.view.gb = &gb; + [self.view screenSizeChanged]; + [self loadROM]; + [self reset:nil]; + +} + +- (void) initMemoryView +{ + hex_controller = [[HFController alloc] init]; + [hex_controller setBytesPerColumn:1]; + [hex_controller setEditMode:HFOverwriteMode]; + + [hex_controller setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]]; + + /* Here we're going to make three representers - one for the hex, one for the ASCII, and one for the scrollbar. To lay these all out properly, we'll use a fourth HFLayoutRepresenter. */ + HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init]; + HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init]; + HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init]; + HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init]; + lineRep = [[HFLineCountingRepresenter alloc] init]; + HFStatusBarRepresenter *statusRep = [[HFStatusBarRepresenter alloc] init]; + + lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal; + + /* Add all our reps to the controller. */ + [hex_controller addRepresenter:layoutRep]; + [hex_controller addRepresenter:hexRep]; + [hex_controller addRepresenter:asciiRep]; + [hex_controller addRepresenter:scrollRep]; + [hex_controller addRepresenter:lineRep]; + [hex_controller addRepresenter:statusRep]; + + /* Tell the layout rep which reps it should lay out. */ + [layoutRep addRepresenter:hexRep]; + [layoutRep addRepresenter:scrollRep]; + [layoutRep addRepresenter:asciiRep]; + [layoutRep addRepresenter:lineRep]; + [layoutRep addRepresenter:statusRep]; + + + [(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + /* Grab the layout rep's view and stick it into our container. */ + NSView *layoutView = [layoutRep view]; + NSRect layoutViewFrame = self.memoryView.frame; + [layoutView setFrame:layoutViewFrame]; + [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin]; + [self.memoryView addSubview:layoutView]; + + self.memoryBankItem.enabled = false; +} + ++ (BOOL)autosavesInPlace +{ + return YES; +} + +- (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"; +} + +- (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type +{ + return YES; +} + +- (void) loadROM +{ + NSString *rom_warnings = [self captureOutputForBlock:^{ + GB_debugger_clear_symbols(&gb); + if ([[self.fileType pathExtension] isEqualToString:@"isx"]) { + GB_load_isx(&gb, [self.fileName UTF8String]); + GB_load_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"ram"] UTF8String]); + + } + 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]); + }]; + if (rom_warnings && !rom_warning_issued) { + rom_warning_issued = true; + [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow]; + } +} + +- (void)close +{ + [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"]; + [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"]; + [self stop]; + [self.consoleWindow close]; + [super close]; +} + +- (IBAction) interrupt:(id)sender +{ + [self log:"^C\n"]; + GB_debugger_break(&gb); + if (!running) { + [self start]; + } + [self.consoleWindow makeKeyAndOrderFront:nil]; + [self.consoleInput becomeFirstResponder]; +} + +- (IBAction)mute:(id)sender +{ + if (self.audioClient.isPlaying) { + [self.audioClient stop]; + } + else { + [self.audioClient start]; + } + [[NSUserDefaults standardUserDefaults] setBool:!self.audioClient.isPlaying forKey:@"Mute"]; +} + +- (BOOL)validateUserInterfaceItem:(id)anItem +{ + if ([anItem action] == @selector(mute:)) { + [(NSMenuItem*)anItem setState:!self.audioClient.isPlaying]; + } + else if ([anItem action] == @selector(togglePause:)) { + [(NSMenuItem*)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))]; + return !GB_debugger_is_stopped(&gb); + } + else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) { + [(NSMenuItem*)anItem setState:anItem.tag == current_model]; + } + else if ([anItem action] == @selector(interrupt:)) { + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { + return false; + } + } + else if ([anItem action] == @selector(disconnectAllAccessories:)) { + [(NSMenuItem*)anItem setState:accessory == GBAccessoryNone]; + } + else if ([anItem action] == @selector(connectPrinter:)) { + [(NSMenuItem*)anItem setState:accessory == GBAccessoryPrinter]; + } + else if ([anItem action] == @selector(connectWorkboy:)) { + [(NSMenuItem*)anItem setState:accessory == GBAccessoryWorkboy]; + } + else if ([anItem action] == @selector(toggleCheats:)) { + [(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)]; + } + return [super validateUserInterfaceItem:anItem]; +} + + +- (void) windowWillEnterFullScreen:(NSNotification *)notification +{ + fullScreen = true; + self.view.mouseHidingEnabled = running; +} + +- (void) windowWillExitFullScreen:(NSNotification *)notification +{ + fullScreen = false; + self.view.mouseHidingEnabled = NO; +} + +- (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame +{ + if (fullScreen) { + return newFrame; + } + size_t width = GB_get_screen_width(&gb), + height = GB_get_screen_height(&gb); + + NSRect rect = window.contentView.frame; + + 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; + + if (rect.size.width > newFrame.size.width) { + rect.size.width = width; + rect.size.height = height + titlebarSize; + } + else if (rect.size.height > newFrame.size.height) { + rect.size.width = width; + rect.size.height = height + titlebarSize; + } + + rect.origin = window.frame.origin; + rect.origin.y -= rect.size.height - window.frame.size.height; + + return rect; +} + +- (void) appendPendingOutput +{ + [console_output_lock lock]; + if (shouldClearSideView) { + shouldClearSideView = false; + [self.debuggerSideView setString:@""]; + } + if (pending_console_output) { + NSTextView *textView = logToSideView? self.debuggerSideView : self.consoleOutput; + + [hex_controller reloadData]; + [self reloadVRAMData: nil]; + + [textView.textStorage appendAttributedString:pending_console_output]; + [textView scrollToEndOfDocument:nil]; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { + [self.consoleWindow orderFront:nil]; + } + pending_console_output = nil; +} + [console_output_lock unlock]; + +} + +- (void) log: (const char *) string withAttributes: (GB_log_attributes) attributes +{ + NSString *nsstring = @(string); // For ref-counting + if (capturedOutput) { + [capturedOutput appendString:nsstring]; + return; + } + + + NSFont *font = [NSFont userFixedPitchFontOfSize:12]; + NSUnderlineStyle underline = NSUnderlineStyleNone; + if (attributes & GB_LOG_BOLD) { + font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask]; + } + + if (attributes & GB_LOG_UNDERLINE_MASK) { + underline = (attributes & GB_LOG_UNDERLINE_MASK) == GB_LOG_DASHED_UNDERLINE? NSUnderlinePatternDot | NSUnderlineStyleSingle : NSUnderlineStyleSingle; + } + + NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init]; + [paragraph_style setLineSpacing:2]; + NSMutableAttributedString *attributed = + [[NSMutableAttributedString alloc] initWithString:nsstring + attributes:@{NSFontAttributeName: font, + NSForegroundColorAttributeName: [NSColor whiteColor], + NSUnderlineStyleAttributeName: @(underline), + NSParagraphStyleAttributeName: paragraph_style}]; + + [console_output_lock lock]; + if (!pending_console_output) { + pending_console_output = attributed; + } + else { + [pending_console_output appendAttributedString:attributed]; + } + + if (![console_output_timer isValid]) { + 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]; + } + + [console_output_lock unlock]; + + /* Make sure mouse is not hidden while debugging */ + self.view.mouseHidingEnabled = NO; +} + +- (IBAction)showConsoleWindow:(id)sender +{ + [self.consoleWindow orderBack:nil]; +} + +- (IBAction)consoleInput:(NSTextField *)sender +{ + NSString *line = [sender stringValue]; + if ([line isEqualToString:@""] && lastConsoleInput) { + line = lastConsoleInput; + } + else if (line) { + lastConsoleInput = line; + } + else { + line = @""; + } + + if (!in_sync_input) { + [self log:">"]; + } + [self log:[line UTF8String]]; + [self log:"\n"]; + [has_debugger_input lock]; + [debugger_input_queue addObject:line]; + [has_debugger_input unlockWithCondition:1]; + + [sender setStringValue:@""]; +} + +- (void) interruptDebugInputRead +{ + [has_debugger_input lock]; + [debugger_input_queue addObject:[NSNull null]]; + [has_debugger_input unlockWithCondition:1]; +} + +- (void) updateSideView +{ + if (!GB_debugger_is_stopped(&gb)) { + return; + } + + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self updateSideView]; + }); + return; + } + + [console_output_lock lock]; + shouldClearSideView = true; + [self appendPendingOutput]; + logToSideView = true; + [console_output_lock unlock]; + + for (NSString *line in [self.debuggerSideViewInput.string componentsSeparatedByString:@"\n"]) { + NSString *stripped = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if ([stripped length]) { + char *dupped = strdup([stripped UTF8String]); + GB_attributed_log(&gb, GB_LOG_BOLD, "%s:\n", dupped); + GB_debugger_execute_command(&gb, dupped); + GB_log(&gb, "\n"); + free(dupped); + } + } + + [console_output_lock lock]; + [self appendPendingOutput]; + logToSideView = false; + [console_output_lock unlock]; +} + +- (char *) getDebuggerInput +{ + [self updateSideView]; + [self log:">"]; + in_sync_input = true; + [has_debugger_input lockWhenCondition:1]; + NSString *input = [debugger_input_queue firstObject]; + [debugger_input_queue removeObjectAtIndex:0]; + [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; + in_sync_input = false; + shouldClearSideView = true; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 10)), dispatch_get_main_queue(), ^{ + if (shouldClearSideView) { + shouldClearSideView = false; + [self.debuggerSideView setString:@""]; + } + }); + if ((id) input == [NSNull null]) { + return NULL; + } + return strdup([input UTF8String]); +} + +- (char *) getAsyncDebuggerInput +{ + [has_debugger_input lock]; + NSString *input = [debugger_input_queue firstObject]; + if (input) { + [debugger_input_queue removeObjectAtIndex:0]; + } + [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; + if ((id)input == [NSNull null]) { + return NULL; + } + return input? strdup([input UTF8String]): NULL; +} + +- (IBAction)saveState:(id)sender +{ + bool __block success = false; + [self performAtomicBlock:^{ + success = GB_save_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]) == 0; + }]; + + if (!success) { + [GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow]; + NSBeep(); + } +} + +- (IBAction)loadState:(id)sender +{ + bool __block success = false; + NSString *error = + [self captureOutputForBlock:^{ + success = GB_load_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]) == 0; + }]; + + if (!success) { + if (error) { + [GBWarningPopover popoverWithContents:error onWindow:self.mainWindow]; + } + NSBeep(); + } +} + +- (IBAction)clearConsole:(id)sender +{ + [self.consoleOutput setString:@""]; +} + +- (void)log:(const char *)log +{ + [self log:log withAttributes:0]; +} + +- (uint8_t) readMemory:(uint16_t)addr +{ + while (!GB_is_inited(&gb)); + return GB_read_memory(&gb, addr); +} + +- (void) writeMemory:(uint16_t)addr value:(uint8_t)value +{ + while (!GB_is_inited(&gb)); + GB_write_memory(&gb, addr, value); +} + +- (void) performAtomicBlock: (void (^)())block +{ + while (!GB_is_inited(&gb)); + bool was_running = running && !GB_debugger_is_stopped(&gb); + if (was_running) { + [self stop]; + } + block(); + if (was_running) { + [self start]; + } +} + +- (NSString *) captureOutputForBlock: (void (^)())block +{ + capturedOutput = [[NSMutableString alloc] init]; + [self performAtomicBlock:block]; + NSString *ret = [capturedOutput stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + capturedOutput = nil; + return [ret length]? ret : nil; +} + ++ (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale +{ + CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef) data); + CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; + CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; + + CGImageRef iref = CGImageCreate(width, + height, + 8, + 32, + 4 * width, + colorSpaceRef, + bitmapInfo, + provider, + NULL, + YES, + renderingIntent); + CGDataProviderRelease(provider); + CGColorSpaceRelease(colorSpaceRef); + + NSImage *ret = [[NSImage alloc] initWithCGImage:iref size:NSMakeSize(width * scale, height * scale)]; + CGImageRelease(iref); + + return ret; +} + +- (void) reloadMemoryView +{ + if (self.memoryWindow.isVisible) { + [hex_controller reloadData]; + } +} + +- (IBAction) reloadVRAMData: (id) sender +{ + if (self.vramWindow.isVisible) { + switch ([self.vramTabView.tabViewItems indexOfObject:self.vramTabView.selectedTabViewItem]) { + case 0: + /* Tileset */ + { + GB_palette_type_t palette_type = GB_PALETTE_NONE; + NSUInteger palette_menu_index = self.tilesetPaletteButton.indexOfSelectedItem; + if (palette_menu_index) { + palette_type = palette_menu_index > 8? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND; + } + size_t bufferLength = 256 * 192 * 4; + NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength]; + data.length = bufferLength; + GB_draw_tileset(&gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 1) & 7); + + self.tilesetImageView.image = [Document imageFromData:data width:256 height:192 scale:1.0]; + self.tilesetImageView.layer.magnificationFilter = kCAFilterNearest; + } + break; + + case 1: + /* Tilemap */ + { + GB_palette_type_t palette_type = GB_PALETTE_NONE; + NSUInteger palette_menu_index = self.tilemapPaletteButton.indexOfSelectedItem; + if (palette_menu_index > 1) { + palette_type = palette_menu_index > 9? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND; + } + else if (palette_menu_index == 1) { + palette_type = GB_PALETTE_AUTO; + } + + size_t bufferLength = 256 * 256 * 4; + NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength]; + data.length = bufferLength; + GB_draw_tilemap(&gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 2) & 7, + (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem, + (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem); + + self.tilemapImageView.scrollRect = NSMakeRect(GB_read_memory(&gb, 0xFF00 | GB_IO_SCX), + GB_read_memory(&gb, 0xFF00 | GB_IO_SCY), + 160, 144); + self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0]; + self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest; + } + break; + + case 2: + /* OAM */ + { + oamCount = GB_get_oam_info(&gb, oamInfo, &oamHeight); + dispatch_async(dispatch_get_main_queue(), ^{ + if (!oamUpdating) { + oamUpdating = true; + [self.spritesTableView reloadData]; + oamUpdating = false; + } + }); + } + break; + + case 3: + /* Palettes */ + { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.paletteTableView reloadData]; + }); + } + break; + } + } +} + +- (IBAction) showMemory:(id)sender +{ + if (!hex_controller) { + [self initMemoryView]; + } + [self.memoryWindow makeKeyAndOrderFront:sender]; +} + +- (IBAction)hexGoTo:(id)sender +{ + NSString *error = [self captureOutputForBlock:^{ + uint16_t addr; + if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, NULL)) { + return; + } + addr -= lineRep.valueOffset; + if (addr >= hex_controller.byteArray.length) { + GB_log(&gb, "Value $%04x is out of range.\n", addr); + return; + } + [hex_controller setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]]; + [hex_controller _ensureVisibilityOfLocation:addr]; + [self.memoryWindow makeFirstResponder:self.memoryView.subviews[0].subviews[0]]; + }]; + if (error) { + NSBeep(); + [GBWarningPopover popoverWithContents:error onView:sender]; + } +} + +- (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors +{ + NSString *error = [self captureOutputForBlock:^{ + uint16_t addr, bank; + if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, &bank)) { + return; + } + + if (bank == (uint16_t) -1) { + bank = addr; + } + + uint16_t n_banks = 1; + switch ([(GBMemoryByteArray *)(hex_controller.byteArray) mode]) { + case GBMemoryROM: { + size_t rom_size; + GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL); + n_banks = rom_size / 0x4000; + break; + } + case GBMemoryVRAM: + n_banks = GB_is_cgb(&gb) ? 2 : 1; + break; + case GBMemoryExternalRAM: { + size_t ram_size; + GB_get_direct_access(&gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL); + n_banks = (ram_size + 0x1FFF) / 0x2000; + break; + } + case GBMemoryRAM: + n_banks = GB_is_cgb(&gb) ? 8 : 1; + break; + case GBMemoryEntireSpace: + break; + } + + bank %= n_banks; + + [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]]; + [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:bank]; + [hex_controller reloadData]; + }]; + + if (error && !ignore_errors) { + NSBeep(); + [GBWarningPopover popoverWithContents:error onView:sender]; + } +} + +- (IBAction)hexUpdateBank:(NSControl *)sender +{ + [self hexUpdateBank:sender ignoreErrors:false]; +} + +- (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender +{ + self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace; + GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(hex_controller.byteArray); + [byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]]; + uint16_t bank; + switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) { + case GBMemoryEntireSpace: + case GBMemoryROM: + lineRep.valueOffset = 0; + GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, NULL, &bank); + byteArray.selectedBank = bank; + break; + case GBMemoryVRAM: + lineRep.valueOffset = 0x8000; + GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank); + byteArray.selectedBank = bank; + break; + case GBMemoryExternalRAM: + lineRep.valueOffset = 0xA000; + GB_get_direct_access(&gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank); + byteArray.selectedBank = bank; + break; + case GBMemoryRAM: + lineRep.valueOffset = 0xC000; + GB_get_direct_access(&gb, GB_DIRECT_ACCESS_RAM, NULL, &bank); + byteArray.selectedBank = bank; + break; + } + [self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]]; + [hex_controller reloadData]; + [self.memoryView setNeedsDisplay:YES]; +} + +- (GB_gameboy_t *) gameboy +{ + return &gb; +} + ++ (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName +{ + return YES; +} + +- (void)cameraRequestUpdate +{ + 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]; + CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[[device formats] firstObject] formatDescription]); + + if (!input) { + GB_camera_updated(&gb); + return; + } + + cameraOutput = [[AVCaptureStillImageOutput alloc] init]; + /* Greyscale is not widely supported, so we use YUV, whose first element is the brightness. */ + [cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat), + (id)kCVPixelBufferWidthKey: @(MAX(128, 112 * dimensions.width / dimensions.height)), + (id)kCVPixelBufferHeightKey: @(MAX(112, 128 * dimensions.height / dimensions.width)),}]; + + + cameraSession = [AVCaptureSession new]; + cameraSession.sessionPreset = AVCaptureSessionPresetPhoto; + + [cameraSession addInput: input]; + [cameraSession addOutput: cameraOutput]; + [cameraSession startRunning]; + cameraConnection = [cameraOutput connectionWithMediaType: AVMediaTypeVideo]; + } + + [cameraOutput captureStillImageAsynchronouslyFromConnection: cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) { + if (error) { + GB_camera_updated(&gb); + } + else { + if (cameraImage) { + CVBufferRelease(cameraImage); + cameraImage = NULL; + } + cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer)); + /* We only need the actual buffer, no need to ever unlock it. */ + CVPixelBufferLockBaseAddress(cameraImage, 0); + } + + GB_camera_updated(&gb); + }]; + } + @catch (NSException *exception) { + /* I have not tested camera support on many devices, so we catch exceptions just in case. */ + GB_camera_updated(&gb); + } + }); +} + +- (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t) y +{ + if (!cameraImage) { + return 0; + } + + uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(cameraImage); + size_t bytesPerRow = CVPixelBufferGetBytesPerRow(cameraImage); + uint8_t offsetX = (CVPixelBufferGetWidth(cameraImage) - 128) / 2; + uint8_t offsetY = (CVPixelBufferGetHeight(cameraImage) - 112) / 2; + uint8_t ret = baseAddress[(x + offsetX) * 2 + (y + offsetY) * bytesPerRow]; + + return ret; +} + +- (IBAction)toggleTilesetGrid:(NSButton *)sender +{ + if (sender.state) { + self.tilesetImageView.horizontalGrids = @[ + [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], + [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:128], + + ]; + self.tilesetImageView.verticalGrids = @[ + [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], + [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:64], + ]; + self.tilemapImageView.horizontalGrids = @[ + [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], + ]; + self.tilemapImageView.verticalGrids = @[ + [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8], + ]; + } + else { + self.tilesetImageView.horizontalGrids = nil; + self.tilesetImageView.verticalGrids = nil; + self.tilemapImageView.horizontalGrids = nil; + self.tilemapImageView.verticalGrids = nil; + } +} + +- (IBAction)toggleScrollingDisplay:(NSButton *)sender +{ + self.tilemapImageView.displayScrollRect = sender.state; +} + +- (IBAction)vramTabChanged:(NSSegmentedControl *)sender +{ + [self.vramTabView selectTabViewItemAtIndex:[sender selectedSegment]]; + [self reloadVRAMData:sender]; + [self.vramTabView.selectedTabViewItem.view addSubview:self.gridButton]; + self.gridButton.hidden = [sender selectedSegment] >= 2; + + NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height; + CGRect window_rect = self.vramWindow.frame; + window_rect.origin.y += window_rect.size.height; + switch ([sender selectedSegment]) { + case 0: + window_rect.size.height = 384 + height_diff + 48; + break; + case 1: + case 2: + window_rect.size.height = 512 + height_diff + 48; + break; + case 3: + window_rect.size.height = 20 * 16 + height_diff + 24; + break; + + default: + break; + } + window_rect.origin.y -= window_rect.size.height; + [self.vramWindow setFrame:window_rect display:YES animate:YES]; +} + +- (void)mouseDidLeaveImageView:(GBImageView *)view +{ + self.vramStatusLabel.stringValue = @""; +} + +- (void)imageView:(GBImageView *)view mouseMovedToX:(NSUInteger)x Y:(NSUInteger)y +{ + if (view == self.tilesetImageView) { + uint8_t bank = x >= 128? 1 : 0; + x &= 127; + uint16_t tile = x / 8 + y / 8 * 16; + self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10]; + } + else if (view == self.tilemapImageView) { + uint16_t map_offset = x / 8 + y / 8 * 32; + uint16_t map_base = 0x1800; + GB_map_type_t map_type = (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem; + GB_tileset_type_t tileset_type = (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem; + uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC]; + uint8_t *vram = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL); + + if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & 0x08)) { + map_base = 0x1c00; + } + + if (tileset_type == GB_TILESET_AUTO) { + tileset_type = (lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000; + } + + uint8_t tile = vram[map_base + map_offset]; + uint16_t tile_address = 0; + if (tileset_type == GB_TILESET_8000) { + tile_address = 0x8000 + tile * 0x10; + } + else { + tile_address = 0x9000 + (int8_t)tile * 0x10; + } + + if (GB_is_cgb(&gb)) { + uint8_t attributes = vram[map_base + map_offset + 0x2000]; + self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)", + tile, + attributes & 0x8? 1 : 0, + tile_address, + 0x8000 + map_base + map_offset, + (attributes & 0x80) ? 'P' : '-', + (attributes & 0x40) ? 'V' : '-', + (attributes & 0x20) ? 'H' : '-', + attributes & 0x8? 1 : 0, + attributes & 0x7 + ]; + } + else { + self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x ($%04x) at map address $%04x", + tile, + tile_address, + 0x8000 + map_base + map_offset + ]; + } + + } +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + if (tableView == self.paletteTableView) { + return 16; /* 8 BG palettes, 8 OBJ palettes*/ + } + else if (tableView == self.spritesTableView) { + return oamCount; + } + return 0; +} + +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + NSUInteger columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + if (tableView == self.paletteTableView) { + if (columnIndex == 0) { + 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); + + uint16_t index = columnIndex - 1 + (row & 7) * 4; + return @((palette_data[(index << 1) + 1] << 8) | palette_data[(index << 1)]); + } + else if (tableView == self.spritesTableView) { + switch (columnIndex) { + case 0: + return [Document imageFromData:[NSData dataWithBytesNoCopy:oamInfo[row].image + length:64 * 4 + freeWhenDone:NO] + width:8 + height:oamHeight + scale:16.0/oamHeight]; + case 1: + return @((unsigned)oamInfo[row].x - 8); + case 2: + return @((unsigned)oamInfo[row].y - 16); + case 3: + return [NSString stringWithFormat:@"$%02x", oamInfo[row].tile]; + case 4: + return [NSString stringWithFormat:@"$%04x", 0x8000 + oamInfo[row].tile * 0x10]; + case 5: + return [NSString stringWithFormat:@"$%04x", oamInfo[row].oam_addr]; + case 6: + if (GB_is_cgb(&gb)) { + return [NSString stringWithFormat:@"%c%c%c%d%d", + oamInfo[row].flags & 0x80? 'P' : '-', + oamInfo[row].flags & 0x40? 'Y' : '-', + oamInfo[row].flags & 0x20? 'X' : '-', + oamInfo[row].flags & 0x08? 1 : 0, + oamInfo[row].flags & 0x07]; + } + return [NSString stringWithFormat:@"%c%c%c%d", + oamInfo[row].flags & 0x80? 'P' : '-', + oamInfo[row].flags & 0x40? 'Y' : '-', + oamInfo[row].flags & 0x20? 'X' : '-', + oamInfo[row].flags & 0x10? 1 : 0]; + case 7: + return oamInfo[row].obscured_by_line_limit? @"Dropped: Too many sprites in line": @""; + + } + } + return nil; +} + +- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row +{ + return tableView == self.spritesTableView; +} + +- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row +{ + return NO; +} + +- (IBAction)showVRAMViewer:(id)sender +{ + [self.vramWindow makeKeyAndOrderFront:sender]; + [self reloadVRAMData: nil]; +} + +- (void) printImage:(uint32_t *)imageBytes height:(unsigned) height + topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin + exposure:(unsigned) exposure +{ + uint32_t paddedImage[160 * (topMargin + height + bottomMargin)]; + memset(paddedImage, 0xFF, sizeof(paddedImage)); + memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0])); + if (!self.printerFeedWindow.isVisible) { + currentPrinterImageData = [[NSMutableData alloc] init]; + } + [currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)]; + /* UI related code must run on main thread. */ + dispatch_async(dispatch_get_main_queue(), ^{ + self.feedImageView.image = [Document imageFromData:currentPrinterImageData + width:160 + height:currentPrinterImageData.length / 160 / sizeof(imageBytes[0]) + scale:2.0]; + NSRect frame = self.printerFeedWindow.frame; + frame.size = self.feedImageView.image.size; + [self.printerFeedWindow setContentMaxSize:frame.size]; + frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height; + [self.printerFeedWindow setFrame:frame display:NO animate: self.printerFeedWindow.isVisible]; + [self.printerFeedWindow orderFront:NULL]; + }); + +} + +- (void)printDocument:(id)sender +{ + if (self.feedImageView.image.size.height == 0) { + NSBeep(); return; + } + NSImageView *view = [[NSImageView alloc] initWithFrame:(NSRect){{0,0}, self.feedImageView.image.size}]; + view.image = self.feedImageView.image; + [[NSPrintOperation printOperationWithView:view] runOperationModalForWindow:self.printerFeedWindow delegate:nil didRunSelector:NULL contextInfo:NULL]; +} + +- (IBAction)savePrinterFeed:(id)sender +{ + bool shouldResume = running; + [self stop]; + NSSavePanel * savePanel = [NSSavePanel savePanel]; + [savePanel setAllowedFileTypes:@[@"png"]]; + [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) { + if (result == NSFileHandlingPanelOKButton) { + [savePanel orderOut:self]; + CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL + context:nil + hints:nil]; + NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef]; + [imageRep setSize:(NSSize){160, self.feedImageView.image.size.height / 2}]; + NSData *data = [imageRep representationUsingType:NSPNGFileType properties:@{}]; + [data writeToURL:savePanel.URL atomically:NO]; + [self.printerFeedWindow setIsVisible:NO]; + } + if (shouldResume) { + [self start]; + } + }]; +} + +- (IBAction)disconnectAllAccessories:(id)sender +{ + [self performAtomicBlock:^{ + accessory = GBAccessoryNone; + GB_disconnect_serial(&gb); + }]; +} + +- (IBAction)connectPrinter:(id)sender +{ + [self performAtomicBlock:^{ + accessory = GBAccessoryPrinter; + GB_connect_printer(&gb, printImage); + }]; +} + +- (IBAction)connectWorkboy:(id)sender +{ + [self performAtomicBlock:^{ + accessory = GBAccessoryWorkboy; + GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime); + }]; +} + +- (void) updateHighpassFilter +{ + if (GB_is_inited(&gb)) { + GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]); + } +} + +- (void) updateColorCorrectionMode +{ + if (GB_is_inited(&gb)) { + GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]); + } +} + +- (void) updateFrameBlendingMode +{ + self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; +} + +- (void) updateRewindLength +{ + [self performAtomicBlock:^{ + if (GB_is_inited(&gb)) { + GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); + } + }]; +} + +- (void)dmgModelChanged +{ + modelsChanging = true; + if (current_model == MODEL_DMG) { + [self reset:nil]; + } + modelsChanging = false; +} + +- (void)sgbModelChanged +{ + modelsChanging = true; + if (current_model == MODEL_SGB) { + [self reset:nil]; + } + modelsChanging = false; +} + +- (void)cgbModelChanged +{ + modelsChanging = true; + if (current_model == MODEL_CGB) { + [self reset:nil]; + } + modelsChanging = false; +} + +- (void)setFileURL:(NSURL *)fileURL +{ + [super setFileURL:fileURL]; + self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[fileURL path] lastPathComponent]]; + +} + +- (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/bsnes/gb/Cocoa/Document.xib b/bsnes/gb/Cocoa/Document.xib new file mode 100644 index 00000000..d02f5bd7 --- /dev/null +++ b/bsnes/gb/Cocoa/Document.xib @@ -0,0 +1,1080 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/GBAudioClient.h b/bsnes/gb/Cocoa/GBAudioClient.h new file mode 100644 index 00000000..aa7be8c2 --- /dev/null +++ b/bsnes/gb/Cocoa/GBAudioClient.h @@ -0,0 +1,12 @@ +#import +#import + +@interface GBAudioClient : NSObject +@property (strong) void (^renderBlock)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer); +@property (readonly) UInt32 rate; +@property (readonly, getter=isPlaying) bool playing; +-(void) start; +-(void) stop; +-(id) initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block + andSampleRate:(UInt32) rate; +@end diff --git a/bsnes/gb/Cocoa/GBAudioClient.m b/bsnes/gb/Cocoa/GBAudioClient.m new file mode 100644 index 00000000..7f2115d7 --- /dev/null +++ b/bsnes/gb/Cocoa/GBAudioClient.m @@ -0,0 +1,111 @@ +#import +#import +#import "GBAudioClient.h" + +static OSStatus render( + GBAudioClient *self, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) + +{ + GB_sample_t *buffer = (GB_sample_t *)ioData->mBuffers[0].mData; + + self.renderBlock(self.rate, inNumberFrames, buffer); + + return noErr; +} + +@implementation GBAudioClient +{ + AudioComponentInstance audioUnit; +} + +-(id) initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block + andSampleRate:(UInt32) rate +{ + if (!(self = [super init])) { + return nil; + } + + // Configure the search parameters to find the default playback output unit + // (called the kAudioUnitSubType_RemoteIO on iOS but + // kAudioUnitSubType_DefaultOutput on Mac OS X) + AudioComponentDescription defaultOutputDescription; + defaultOutputDescription.componentType = kAudioUnitType_Output; + defaultOutputDescription.componentSubType = kAudioUnitSubType_DefaultOutput; + defaultOutputDescription.componentManufacturer = kAudioUnitManufacturer_Apple; + defaultOutputDescription.componentFlags = 0; + defaultOutputDescription.componentFlagsMask = 0; + + // Get the default playback output unit + AudioComponent defaultOutput = AudioComponentFindNext(NULL, &defaultOutputDescription); + NSAssert(defaultOutput, @"Can't find default output"); + + // Create a new unit based on this that we'll use for output + OSErr err = AudioComponentInstanceNew(defaultOutput, &audioUnit); + NSAssert1(audioUnit, @"Error creating unit: %hd", err); + + // Set our tone rendering function on the unit + AURenderCallbackStruct input; + input.inputProc = (void*)render; + input.inputProcRefCon = (__bridge void * _Nullable)(self); + err = AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + 0, + &input, + sizeof(input)); + NSAssert1(err == noErr, @"Error setting callback: %hd", err); + + AudioStreamBasicDescription streamFormat; + streamFormat.mSampleRate = rate; + streamFormat.mFormatID = kAudioFormatLinearPCM; + streamFormat.mFormatFlags = + kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked | kAudioFormatFlagsNativeEndian; + streamFormat.mBytesPerPacket = 4; + streamFormat.mFramesPerPacket = 1; + streamFormat.mBytesPerFrame = 4; + streamFormat.mChannelsPerFrame = 2; + streamFormat.mBitsPerChannel = 2 * 8; + err = AudioUnitSetProperty (audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 0, + &streamFormat, + sizeof(AudioStreamBasicDescription)); + NSAssert1(err == noErr, @"Error setting stream format: %hd", err); + err = AudioUnitInitialize(audioUnit); + NSAssert1(err == noErr, @"Error initializing unit: %hd", err); + + self.renderBlock = block; + _rate = rate; + + return self; +} + +-(void) start +{ + OSErr err = AudioOutputUnitStart(audioUnit); + NSAssert1(err == noErr, @"Error starting unit: %hd", err); + _playing = YES; + +} + + +-(void) stop +{ + AudioOutputUnitStop(audioUnit); + _playing = NO; +} + +-(void) dealloc +{ + [self stop]; + AudioUnitUninitialize(audioUnit); + AudioComponentInstanceDispose(audioUnit); +} + +@end \ No newline at end of file diff --git a/bsnes/gb/Cocoa/GBBorderView.h b/bsnes/gb/Cocoa/GBBorderView.h new file mode 100644 index 00000000..477add17 --- /dev/null +++ b/bsnes/gb/Cocoa/GBBorderView.h @@ -0,0 +1,5 @@ +#import + +@interface GBBorderView : NSView + +@end diff --git a/bsnes/gb/Cocoa/GBBorderView.m b/bsnes/gb/Cocoa/GBBorderView.m new file mode 100644 index 00000000..a5f5e817 --- /dev/null +++ b/bsnes/gb/Cocoa/GBBorderView.m @@ -0,0 +1,26 @@ +#import "GBBorderView.h" + +@implementation GBBorderView + + +- (void)awakeFromNib +{ + self.wantsLayer = YES; +} + +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +- (void)updateLayer +{ + /* Wonderful, wonderful windowserver(?) bug. Using 0,0,0 here would cause it to render garbage + on fullscreen windows on some High Sierra machines. Any other value, including the one used + here (which is rendered exactly the same due to rounding) works around this bug. */ + self.layer.backgroundColor = [NSColor colorWithCalibratedRed:0 + green:0 + blue:1.0 / 1024.0 + alpha:1.0].CGColor; +} +@end diff --git a/bsnes/gb/Cocoa/GBButtons.h b/bsnes/gb/Cocoa/GBButtons.h new file mode 100644 index 00000000..1f8b5afb --- /dev/null +++ b/bsnes/gb/Cocoa/GBButtons.h @@ -0,0 +1,35 @@ +#ifndef GBButtons_h +#define GBButtons_h + +typedef enum : NSUInteger { + GBRight, + GBLeft, + GBUp, + GBDown, + GBA, + GBB, + GBSelect, + GBStart, + GBTurbo, + GBRewind, + GBUnderclock, + GBButtonCount, + GBGameBoyButtonCount = GBStart + 1, +} GBButton; + +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) { + return [NSString stringWithFormat:@"GBPlayer%d%@", player + 1, GBButtonNames[button]]; + } + return [NSString stringWithFormat:@"GB%@", GBButtonNames[button]]; +} + +#endif diff --git a/bsnes/gb/Cocoa/GBButtons.m b/bsnes/gb/Cocoa/GBButtons.m new file mode 100644 index 00000000..044e9332 --- /dev/null +++ b/bsnes/gb/Cocoa/GBButtons.m @@ -0,0 +1,4 @@ +#import +#import "GBButtons.h" + +NSString const *GBButtonNames[] = {@"Right", @"Left", @"Up", @"Down", @"A", @"B", @"Select", @"Start", @"Turbo", @"Rewind", @"Slow-Motion"}; diff --git a/bsnes/gb/Cocoa/GBCheatTextFieldCell.h b/bsnes/gb/Cocoa/GBCheatTextFieldCell.h new file mode 100644 index 00000000..473e0f30 --- /dev/null +++ b/bsnes/gb/Cocoa/GBCheatTextFieldCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBCheatTextFieldCell : NSTextFieldCell +@property bool usesAddressFormat; +@end diff --git a/bsnes/gb/Cocoa/GBCheatTextFieldCell.m b/bsnes/gb/Cocoa/GBCheatTextFieldCell.m new file mode 100644 index 00000000..611cadeb --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBCheatWindowController.h b/bsnes/gb/Cocoa/GBCheatWindowController.h new file mode 100644 index 00000000..f70553e6 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBCheatWindowController.m b/bsnes/gb/Cocoa/GBCheatWindowController.m new file mode 100644 index 00000000..c10e2a94 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBColorCell.h b/bsnes/gb/Cocoa/GBColorCell.h new file mode 100644 index 00000000..a622c788 --- /dev/null +++ b/bsnes/gb/Cocoa/GBColorCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBColorCell : NSTextFieldCell + +@end diff --git a/bsnes/gb/Cocoa/GBColorCell.m b/bsnes/gb/Cocoa/GBColorCell.m new file mode 100644 index 00000000..0ad102a6 --- /dev/null +++ b/bsnes/gb/Cocoa/GBColorCell.m @@ -0,0 +1,48 @@ +#import "GBColorCell.h" + +static inline double scale_channel(uint8_t x) +{ + x &= 0x1f; + return x / 31.0; +} + +@implementation GBColorCell +{ + NSInteger _integerValue; +} + +- (void)setObjectValue:(id)objectValue +{ + + _integerValue = [objectValue integerValue]; + uint8_t r = _integerValue & 0x1F, + g = (_integerValue >> 5) & 0x1F, + b = (_integerValue >> 10) & 0x1F; + super.objectValue = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"$%04x", (uint16_t)(_integerValue & 0x7FFF)] attributes:@{ + NSForegroundColorAttributeName: r * 3 + g * 4 + b * 2 > 120? [NSColor blackColor] : [NSColor whiteColor] + }]; +} + +- (NSInteger)integerValue +{ + return _integerValue; +} + +- (int)intValue +{ + return (int)_integerValue; +} + + +- (NSColor *) backgroundColor +{ + uint16_t color = self.integerValue; + return [NSColor colorWithRed:scale_channel(color) green:scale_channel(color >> 5) blue:scale_channel(color >> 10) alpha:1.0]; +} + +- (BOOL)drawsBackground +{ + return YES; +} + +@end diff --git a/bsnes/gb/Cocoa/GBCompleteByteSlice.h b/bsnes/gb/Cocoa/GBCompleteByteSlice.h new file mode 100644 index 00000000..24f3ba00 --- /dev/null +++ b/bsnes/gb/Cocoa/GBCompleteByteSlice.h @@ -0,0 +1,7 @@ +#import "Document.h" +#import "HexFiend/HexFiend.h" +#import "HexFiend/HFByteSlice.h" + +@interface GBCompleteByteSlice : HFByteSlice +- (instancetype) initWithByteArray:(HFByteArray *)array; +@end diff --git a/bsnes/gb/Cocoa/GBCompleteByteSlice.m b/bsnes/gb/Cocoa/GBCompleteByteSlice.m new file mode 100644 index 00000000..44e7ee69 --- /dev/null +++ b/bsnes/gb/Cocoa/GBCompleteByteSlice.m @@ -0,0 +1,26 @@ +#import "GBCompleteByteSlice.h" + +@implementation GBCompleteByteSlice +{ + HFByteArray *_array; +} + +- (instancetype) initWithByteArray:(HFByteArray *)array +{ + if ((self = [super init])) { + _array = array; + } + return self; +} + +- (unsigned long long)length +{ + return [_array length]; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range +{ + [_array copyBytes:dst range:range]; +} + +@end diff --git a/bsnes/gb/Cocoa/GBGLShader.h b/bsnes/gb/Cocoa/GBGLShader.h new file mode 100644 index 00000000..8e46f93f --- /dev/null +++ b/bsnes/gb/Cocoa/GBGLShader.h @@ -0,0 +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 withBlendingMode: (GB_frame_blending_mode_t)blendingMode; +@end diff --git a/bsnes/gb/Cocoa/GBGLShader.m b/bsnes/gb/Cocoa/GBGLShader.m new file mode 100644 index 00000000..920226b6 --- /dev/null +++ b/bsnes/gb/Cocoa/GBGLShader.m @@ -0,0 +1,190 @@ +#import "GBGLShader.h" +#import + +/* + Loosely based of https://www.raywenderlich.com/70208/opengl-es-pixel-shaders-tutorial + + This code probably makes no sense after I upgraded it to OpenGL 3, since OpenGL makes aboslute no sense and has zero + helpful documentation. + */ + +static NSString * const vertex_shader = @"\n\ +#version 150 \n\ +in vec4 aPosition;\n\ +void main(void) {\n\ + gl_Position = aPosition;\n\ +}\n\ +"; + +@implementation GBGLShader +{ + GLuint resolution_uniform; + GLuint texture_uniform; + GLuint previous_texture_uniform; + GLuint frame_blending_mode_uniform; + + GLuint position_attribute; + GLuint texture; + GLuint previous_texture; + GLuint program; +} + ++ (NSString *) shaderSourceForName:(NSString *) name +{ + return [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name + ofType:@"fsh" + inDirectory:@"Shaders"] + encoding:NSUTF8StringEncoding + error:nil]; +} + +- (instancetype)initWithName:(NSString *) shaderName +{ + self = [super init]; + if (self) { + // Program + NSString *fragment_shader = [[self class] shaderSourceForName:@"MasterShader"]; + fragment_shader = [fragment_shader stringByReplacingOccurrencesOfString:@"{filter}" + withString:[[self class] shaderSourceForName:shaderName]]; + program = [[self class] programWithVertexShader:vertex_shader fragmentShader:fragment_shader]; + // Attributes + position_attribute = glGetAttribLocation(program, "aPosition"); + // Uniforms + resolution_uniform = glGetUniformLocation(program, "output_resolution"); + + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + texture_uniform = glGetUniformLocation(program, "image"); + + glGenTextures(1, &previous_texture); + glBindTexture(GL_TEXTURE_2D, previous_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + previous_texture_uniform = glGetUniformLocation(program, "previous_image"); + + frame_blending_mode_uniform = glGetUniformLocation(program, "frame_blending_mode"); + + // Configure OpenGL + [self configureOpenGL]; + + } + return self; +} + +- (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); + glActiveTexture(GL_TEXTURE0); + 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(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); + glUniform1i(previous_texture_uniform, 1); + } + glBindFragDataLocation(program, 0, "frag_color"); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +- (void)configureOpenGL +{ + // Program + + glUseProgram(program); + + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + GLuint vbo; + glGenBuffers(1, &vbo); + + // Attributes + + + static GLfloat const quad[16] = { + -1.f, -1.f, 0, 1, + -1.f, +1.f, 0, 1, + +1.f, -1.f, 0, 1, + +1.f, +1.f, 0, 1, + }; + + + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW); + glEnableVertexAttribArray(position_attribute); + glVertexAttribPointer(position_attribute, 4, GL_FLOAT, GL_FALSE, 0, 0); +} + ++ (GLuint)programWithVertexShader:(NSString*)vsh fragmentShader:(NSString*)fsh +{ + // Build shaders + GLuint vertex_shader = [self shaderWithContents:vsh type:GL_VERTEX_SHADER]; + GLuint fragment_shader = [self shaderWithContents:fsh type:GL_FRAGMENT_SHADER]; + // Create program + GLuint program = glCreateProgram(); + // Attach shaders + glAttachShader(program, vertex_shader); + glAttachShader(program, fragment_shader); + // Link program + glLinkProgram(program); + // Check for errors + GLint status; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if (status == GL_FALSE) { + GLchar messages[1024]; + glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]); + NSLog(@"%@:- GLSL Program Error: %s", self, messages); + } + // Delete shaders + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + return program; +} + +- (void)dealloc +{ + glDeleteProgram(program); + glDeleteTextures(1, &texture); + glDeleteTextures(1, &previous_texture); + + /* OpenGL is black magic. Closing one view causes others to be completely black unless we reload their shaders */ + /* We're probably not freeing thing in the right place. */ + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil]; +} + ++ (GLuint)shaderWithContents:(NSString*)contents type:(GLenum)type +{ + + const GLchar *source = [contents UTF8String]; + // Create the shader object + GLuint shader = glCreateShader(type); + // Load the shader source + glShaderSource(shader, 1, &source, 0); + // Compile the shader + glCompileShader(shader); + // Check for errors + GLint status = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (status == GL_FALSE) { + GLchar messages[1024]; + glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]); + NSLog(@"%@:- GLSL Shader Error: %s", self, messages); + } + return shader; +} + +@end diff --git a/bsnes/gb/Cocoa/GBImageCell.h b/bsnes/gb/Cocoa/GBImageCell.h new file mode 100644 index 00000000..0323b41d --- /dev/null +++ b/bsnes/gb/Cocoa/GBImageCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBImageCell : NSImageCell + +@end diff --git a/bsnes/gb/Cocoa/GBImageCell.m b/bsnes/gb/Cocoa/GBImageCell.m new file mode 100644 index 00000000..de75e0e9 --- /dev/null +++ b/bsnes/gb/Cocoa/GBImageCell.m @@ -0,0 +1,10 @@ +#import "GBImageCell.h" + +@implementation GBImageCell +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; + CGContextSetInterpolationQuality(context, kCGInterpolationNone); + [super drawWithFrame:cellFrame inView:controlView]; +} +@end diff --git a/bsnes/gb/Cocoa/GBImageView.h b/bsnes/gb/Cocoa/GBImageView.h new file mode 100644 index 00000000..d5ee534b --- /dev/null +++ b/bsnes/gb/Cocoa/GBImageView.h @@ -0,0 +1,23 @@ +#import + +@protocol GBImageViewDelegate; + +@interface GBImageViewGridConfiguration : NSObject +@property NSColor *color; +@property NSUInteger size; +- (instancetype) initWithColor: (NSColor *) color size: (NSUInteger) size; +@end + +@interface GBImageView : NSImageView +@property (nonatomic) NSArray *horizontalGrids; +@property (nonatomic) NSArray *verticalGrids; +@property (nonatomic) bool displayScrollRect; +@property NSRect scrollRect; +@property (weak) IBOutlet id delegate; +@end + +@protocol GBImageViewDelegate +@optional +- (void) mouseDidLeaveImageView: (GBImageView *)view; +- (void) imageView: (GBImageView *)view mouseMovedToX:(NSUInteger) x Y:(NSUInteger) y; +@end diff --git a/bsnes/gb/Cocoa/GBImageView.m b/bsnes/gb/Cocoa/GBImageView.m new file mode 100644 index 00000000..3525e72e --- /dev/null +++ b/bsnes/gb/Cocoa/GBImageView.m @@ -0,0 +1,127 @@ +#import "GBImageView.h" + +@implementation GBImageViewGridConfiguration +- (instancetype)initWithColor:(NSColor *)color size:(NSUInteger)size +{ + self = [super init]; + self.color = color; + self.size = size; + return self; +} +@end + +@implementation GBImageView +{ + NSTrackingArea *trackingArea; +} +- (void)drawRect:(NSRect)dirtyRect +{ + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; + CGContextSetInterpolationQuality(context, kCGInterpolationNone); + [super drawRect:dirtyRect]; + CGFloat y_ratio = self.frame.size.height / self.image.size.height; + CGFloat x_ratio = self.frame.size.width / self.image.size.width; + for (GBImageViewGridConfiguration *conf in self.verticalGrids) { + [conf.color set]; + for (CGFloat y = conf.size * y_ratio; y < self.frame.size.height; y += conf.size * y_ratio) { + NSBezierPath *line = [NSBezierPath bezierPath]; + [line moveToPoint:NSMakePoint(0, y - 0.5)]; + [line lineToPoint:NSMakePoint(self.frame.size.width, y - 0.5)]; + [line setLineWidth:1.0]; + [line stroke]; + } + } + + for (GBImageViewGridConfiguration *conf in self.horizontalGrids) { + [conf.color set]; + for (CGFloat x = conf.size * x_ratio; x < self.frame.size.width; x += conf.size * x_ratio) { + NSBezierPath *line = [NSBezierPath bezierPath]; + [line moveToPoint:NSMakePoint(x + 0.5, 0)]; + [line lineToPoint:NSMakePoint(x + 0.5, self.frame.size.height)]; + [line setLineWidth:1.0]; + [line stroke]; + } + } + + if (self.displayScrollRect) { + NSBezierPath *path = [NSBezierPath bezierPathWithRect:CGRectInfinite]; + for (unsigned x = 0; x < 2; x++) { + for (unsigned y = 0; y < 2; y++) { + NSRect rect = self.scrollRect; + rect.origin.x *= x_ratio; + rect.origin.y *= y_ratio; + rect.size.width *= x_ratio; + rect.size.height *= y_ratio; + rect.origin.y = self.frame.size.height - rect.origin.y - rect.size.height; + + rect.origin.x -= self.frame.size.width * x; + rect.origin.y += self.frame.size.height * y; + + + NSBezierPath *subpath = [NSBezierPath bezierPathWithRect:rect]; + [path appendBezierPath:subpath]; + } + } + [path setWindingRule:NSEvenOddWindingRule]; + [path setLineWidth:4.0]; + [path setLineJoinStyle:NSRoundLineJoinStyle]; + [[NSColor colorWithWhite:0.2 alpha:0.5] set]; + [path fill]; + [path addClip]; + [[NSColor colorWithWhite:0.0 alpha:0.6] set]; + [path stroke]; + } +} + +- (void)setHorizontalGrids:(NSArray *)horizontalGrids +{ + self->_horizontalGrids = horizontalGrids; + [self setNeedsDisplay]; +} + +- (void)setVerticalGrids:(NSArray *)verticalGrids +{ + self->_verticalGrids = verticalGrids; + [self setNeedsDisplay]; +} + +- (void)setDisplayScrollRect:(bool)displayScrollRect +{ + self->_displayScrollRect = displayScrollRect; + [self setNeedsDisplay]; +} + +- (void)updateTrackingAreas +{ + if (trackingArea != nil) { + [self removeTrackingArea:trackingArea]; + } + + trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds] + options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingMouseMoved + owner:self + userInfo:nil]; + [self addTrackingArea:trackingArea]; +} + +- (void)mouseExited:(NSEvent *)theEvent +{ + if ([self.delegate respondsToSelector:@selector(mouseDidLeaveImageView:)]) { + [self.delegate mouseDidLeaveImageView:self]; + } +} + +- (void)mouseMoved:(NSEvent *)theEvent +{ + if ([self.delegate respondsToSelector:@selector(imageView:mouseMovedToX:Y:)]) { + NSPoint location = [self convertPoint:theEvent.locationInWindow fromView:nil]; + location.x /= self.bounds.size.width; + location.y /= self.bounds.size.height; + location.y = 1 - location.y; + location.x *= self.image.size.width; + location.y *= self.image.size.height; + [self.delegate imageView:self mouseMovedToX:(NSUInteger)location.x Y:(NSUInteger)location.y]; + } +} + +@end diff --git a/bsnes/gb/Cocoa/GBMemoryByteArray.h b/bsnes/gb/Cocoa/GBMemoryByteArray.h new file mode 100644 index 00000000..e3ed71f8 --- /dev/null +++ b/bsnes/gb/Cocoa/GBMemoryByteArray.h @@ -0,0 +1,17 @@ +#import "Document.h" +#import "HexFiend/HexFiend.h" +#import "HexFiend/HFByteArray.h" + +typedef enum { + GBMemoryEntireSpace, + GBMemoryROM, + GBMemoryVRAM, + GBMemoryExternalRAM, + GBMemoryRAM +} GB_memory_mode_t; + +@interface GBMemoryByteArray : HFByteArray +- (instancetype) initWithDocument:(Document *)document; +@property uint16_t selectedBank; +@property GB_memory_mode_t mode; +@end diff --git a/bsnes/gb/Cocoa/GBMemoryByteArray.m b/bsnes/gb/Cocoa/GBMemoryByteArray.m new file mode 100644 index 00000000..32526ade --- /dev/null +++ b/bsnes/gb/Cocoa/GBMemoryByteArray.m @@ -0,0 +1,177 @@ +#define GB_INTERNAL // Todo: Some memory accesses are being done using the struct directly +#import "GBMemoryByteArray.h" +#import "GBCompleteByteSlice.h" + + +@implementation GBMemoryByteArray +{ + Document *_document; +} + +- (instancetype) initWithDocument:(Document *)document +{ + if ((self = [super init])) { + _document = document; + } + return self; +} + +- (unsigned long long)length +{ + switch (_mode) { + case GBMemoryEntireSpace: + return 0x10000; + case GBMemoryROM: + return 0x8000; + case GBMemoryVRAM: + return 0x2000; + case GBMemoryExternalRAM: + return 0x2000; + case GBMemoryRAM: + return 0x2000; + } +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range +{ + __block uint16_t addr = (uint16_t) range.location; + __block unsigned long long length = range.length; + if (_mode == GBMemoryEntireSpace) { + while (length) { + *(dst++) = [_document readMemory:addr++]; + length--; + } + } + else { + [_document performAtomicBlock:^{ + unsigned char *_dst = dst; + uint16_t bank_backup = 0; + GB_gameboy_t *gb = _document.gameboy; + switch (_mode) { + case GBMemoryROM: + bank_backup = gb->mbc_rom_bank; + gb->mbc_rom_bank = self.selectedBank; + break; + case GBMemoryVRAM: + bank_backup = gb->cgb_vram_bank; + if (GB_is_cgb(gb)) { + gb->cgb_vram_bank = self.selectedBank; + } + addr += 0x8000; + break; + case GBMemoryExternalRAM: + bank_backup = gb->mbc_ram_bank; + gb->mbc_ram_bank = self.selectedBank; + addr += 0xA000; + break; + case GBMemoryRAM: + bank_backup = gb->cgb_ram_bank; + if (GB_is_cgb(gb)) { + gb->cgb_ram_bank = self.selectedBank; + } + addr += 0xC000; + break; + default: + assert(false); + } + while (length) { + *(_dst++) = [_document readMemory:addr++]; + length--; + } + switch (_mode) { + case GBMemoryROM: + gb->mbc_rom_bank = bank_backup; + break; + case GBMemoryVRAM: + gb->cgb_vram_bank = bank_backup; + break; + case GBMemoryExternalRAM: + gb->mbc_ram_bank = bank_backup; + break; + case GBMemoryRAM: + gb->cgb_ram_bank = bank_backup; + break; + default: + assert(false); + } + }]; + } +} + +- (NSArray *)byteSlices +{ + return @[[[GBCompleteByteSlice alloc] initWithByteArray:self]]; +} + +- (HFByteArray *)subarrayWithRange:(HFRange)range +{ + unsigned char arr[range.length]; + [self copyBytes:arr range:range]; + HFByteArray *ret = [[HFBTreeByteArray alloc] init]; + HFFullMemoryByteSlice *slice = [[HFFullMemoryByteSlice alloc] initWithData:[NSData dataWithBytes:arr length:range.length]]; + [ret insertByteSlice:slice inRange:HFRangeMake(0, 0)]; + return ret; +} + +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange +{ + if (slice.length != lrange.length) return; /* Insertion is not allowed, only overwriting. */ + [_document performAtomicBlock:^{ + uint16_t addr = (uint16_t) lrange.location; + uint16_t bank_backup = 0; + GB_gameboy_t *gb = _document.gameboy; + switch (_mode) { + case GBMemoryROM: + bank_backup = gb->mbc_rom_bank; + gb->mbc_rom_bank = self.selectedBank; + break; + case GBMemoryVRAM: + bank_backup = gb->cgb_vram_bank; + if (GB_is_cgb(gb)) { + gb->cgb_vram_bank = self.selectedBank; + } + addr += 0x8000; + break; + case GBMemoryExternalRAM: + bank_backup = gb->mbc_ram_bank; + gb->mbc_ram_bank = self.selectedBank; + addr += 0xA000; + break; + case GBMemoryRAM: + bank_backup = gb->cgb_ram_bank; + if (GB_is_cgb(gb)) { + gb->cgb_ram_bank = self.selectedBank; + } + addr += 0xC000; + break; + default: + break; + } + uint8_t values[lrange.length]; + [slice copyBytes:values range:HFRangeMake(0, lrange.length)]; + uint8_t *src = values; + unsigned long long length = lrange.length; + while (length) { + [_document writeMemory:addr++ value:*(src++)]; + length--; + } + switch (_mode) { + case GBMemoryROM: + gb->mbc_rom_bank = bank_backup; + break; + case GBMemoryVRAM: + gb->cgb_vram_bank = bank_backup; + break; + case GBMemoryExternalRAM: + gb->mbc_ram_bank = bank_backup; + break; + case GBMemoryRAM: + gb->cgb_ram_bank = bank_backup; + break; + default: + break; + } + }]; +} + +@end diff --git a/bsnes/gb/Cocoa/GBOpenGLView.h b/bsnes/gb/Cocoa/GBOpenGLView.h new file mode 100644 index 00000000..5f875ab2 --- /dev/null +++ b/bsnes/gb/Cocoa/GBOpenGLView.h @@ -0,0 +1,6 @@ +#import +#import "GBGLShader.h" + +@interface GBOpenGLView : NSOpenGLView +@property GBGLShader *shader; +@end diff --git a/bsnes/gb/Cocoa/GBOpenGLView.m b/bsnes/gb/Cocoa/GBOpenGLView.m new file mode 100644 index 00000000..90ebf8d5 --- /dev/null +++ b/bsnes/gb/Cocoa/GBOpenGLView.m @@ -0,0 +1,39 @@ +#import "GBOpenGLView.h" +#import "GBView.h" +#include + +@implementation GBOpenGLView + +- (void)drawRect:(NSRect)dirtyRect +{ + if (!self.shader) { + self.shader = [[GBGLShader alloc] initWithName:[[NSUserDefaults standardUserDefaults] objectForKey:@"GBFilter"]]; + } + + GBView *gbview = (GBView *)self.superview; + double scale = self.window.backingScaleFactor; + glViewport(0, 0, self.bounds.size.width * scale, self.bounds.size.height * 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(); +} + +- (instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(NSOpenGLPixelFormat *)format +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(filterChanged) name:@"GBFilterChanged" object:nil]; + return [super initWithFrame:frameRect pixelFormat:format]; +} + +- (void) filterChanged +{ + self.shader = nil; + [self setNeedsDisplay:YES]; +} +@end diff --git a/bsnes/gb/Cocoa/GBOptionalVisualEffectView.h b/bsnes/gb/Cocoa/GBOptionalVisualEffectView.h new file mode 100644 index 00000000..13550715 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBOptionalVisualEffectView.m b/bsnes/gb/Cocoa/GBOptionalVisualEffectView.m new file mode 100644 index 00000000..c28eb595 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBPreferencesWindow.h b/bsnes/gb/Cocoa/GBPreferencesWindow.h new file mode 100644 index 00000000..ee697a8d --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferencesWindow.h @@ -0,0 +1,27 @@ +#import +#import + +@interface GBPreferencesWindow : NSWindow +@property IBOutlet NSTableView *controlsTableView; +@property IBOutlet NSPopUpButton *graphicsFilterPopupButton; +@property (strong) IBOutlet NSButton *analogControlsCheckbox; +@property (strong) IBOutlet NSButton *aspectRatioCheckbox; +@property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton; +@property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton; +@property (strong) IBOutlet NSPopUpButton *frameBlendingModePopupButton; +@property (strong) IBOutlet NSPopUpButton *colorPalettePopupButton; +@property (strong) IBOutlet NSPopUpButton *displayBorderPopupButton; +@property (strong) IBOutlet NSPopUpButton *rewindPopupButton; +@property (strong) IBOutlet NSButton *configureJoypadButton; +@property (strong) IBOutlet NSButton *skipButton; +@property (strong) IBOutlet NSMenuItem *bootROMsFolderItem; +@property (strong) IBOutlet NSPopUpButtonCell *bootROMsButton; +@property (strong) IBOutlet NSPopUpButton *rumbleModePopupButton; + +@property (weak) IBOutlet NSPopUpButton *dmgPopupButton; +@property (weak) IBOutlet NSPopUpButton *sgbPopupButton; +@property (weak) IBOutlet NSPopUpButton *cgbPopupButton; +@property (weak) IBOutlet NSPopUpButton *preferredJoypadButton; +@property (weak) IBOutlet NSPopUpButton *playerListButton; + +@end diff --git a/bsnes/gb/Cocoa/GBPreferencesWindow.m b/bsnes/gb/Cocoa/GBPreferencesWindow.m new file mode 100644 index 00000000..491f0c0f --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferencesWindow.m @@ -0,0 +1,647 @@ +#import "GBPreferencesWindow.h" +#import "NSString+StringForKey.h" +#import "GBButtons.h" +#import "BigSurToolbar.h" +#import + +@implementation GBPreferencesWindow +{ + bool is_button_being_modified; + NSInteger button_being_modified; + signed joystick_configuration_state; + NSString *joystick_being_configured; + 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 +{ + /* The filter list as ordered in the popup button */ + static NSArray * filters = nil; + if (!filters) { + filters = @[ + @"NearestNeighbor", + @"Bilinear", + @"SmoothBilinear", + @"MonoLCD", + @"LCD", + @"CRT", + @"Scale2x", + @"Scale4x", + @"AAScale2x", + @"AAScale4x", + @"HQ2x", + @"OmniScale", + @"OmniScaleLegacy", + @"AAOmniScaleLegacy", + ]; + } + return filters; +} + +- (NSWindowToolbarStyle)toolbarStyle +{ + return NSWindowToolbarStylePreference; +} + +- (void)close +{ + joystick_configuration_state = -1; + [self.configureJoypadButton setEnabled:YES]; + [self.skipButton setEnabled:NO]; + [self.configureJoypadButton setTitle:@"Configure Controller"]; + [super close]; +} + +- (NSPopUpButton *)graphicsFilterPopupButton +{ + return _graphicsFilterPopupButton; +} + +- (void)setGraphicsFilterPopupButton:(NSPopUpButton *)graphicsFilterPopupButton +{ + _graphicsFilterPopupButton = graphicsFilterPopupButton; + NSString *filter = [[NSUserDefaults standardUserDefaults] objectForKey:@"GBFilter"]; + [_graphicsFilterPopupButton selectItemAtIndex:[[[self class] filterList] indexOfObject:filter]]; +} + +- (NSPopUpButton *)highpassFilterPopupButton +{ + return _highpassFilterPopupButton; +} + +- (void)setColorCorrectionPopupButton:(NSPopUpButton *)colorCorrectionPopupButton +{ + _colorCorrectionPopupButton = colorCorrectionPopupButton; + NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]; + [_colorCorrectionPopupButton selectItemAtIndex:mode]; +} + +- (NSPopUpButton *)colorCorrectionPopupButton +{ + 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; + NSInteger length = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]; + [_rewindPopupButton selectItemWithTag:length]; +} + +- (NSPopUpButton *)rewindPopupButton +{ + return _rewindPopupButton; +} + +- (void)setHighpassFilterPopupButton:(NSPopUpButton *)highpassFilterPopupButton +{ + _highpassFilterPopupButton = highpassFilterPopupButton; + [_highpassFilterPopupButton selectItemAtIndex:[[[NSUserDefaults standardUserDefaults] objectForKey:@"GBHighpassFilter"] unsignedIntegerValue]]; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + if (self.playerListButton.selectedTag == 0) { + return GBButtonCount; + } + return GBGameBoyButtonCount; +} + +- (unsigned) usesForKey:(unsigned) key +{ + unsigned ret = 0; + for (unsigned player = 4; player--;) { + for (unsigned button = player == 0? GBButtonCount:GBGameBoyButtonCount; button--;) { + NSNumber *other = [[NSUserDefaults standardUserDefaults] valueForKey:button_to_preference_name(button, player)]; + if (other && [other unsignedIntValue] == key) { + ret++; + } + } + } + return ret; +} + +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + if ([tableColumn.identifier isEqualToString:@"keyName"]) { + return GBButtonNames[row]; + } + + if (is_button_being_modified && button_being_modified == row) { + return @"Select a new key..."; + } + + NSNumber *key = [[NSUserDefaults standardUserDefaults] valueForKey:button_to_preference_name(row, self.playerListButton.selectedTag)]; + if (key) { + if ([self usesForKey:[key unsignedIntValue]] > 1) { + return [[NSAttributedString alloc] initWithString:[NSString displayStringForKeyCode: [key unsignedIntegerValue]] + attributes:@{NSForegroundColorAttributeName: [NSColor colorWithRed:0.9375 green:0.25 blue:0.25 alpha:1.0], + NSFontAttributeName: [NSFont boldSystemFontOfSize:[NSFont systemFontSize]] + }]; + } + return [NSString displayStringForKeyCode: [key unsignedIntegerValue]]; + } + + return @""; +} + +- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + + dispatch_async(dispatch_get_main_queue(), ^{ + is_button_being_modified = true; + button_being_modified = row; + tableView.enabled = NO; + self.playerListButton.enabled = NO; + [tableView reloadData]; + [self makeFirstResponder:self]; + }); + return NO; +} + +-(void)keyDown:(NSEvent *)theEvent +{ + if (!is_button_being_modified) { + if (self.firstResponder != self.controlsTableView && [theEvent type] != NSEventTypeFlagsChanged) { + [super keyDown:theEvent]; + } + return; + } + + is_button_being_modified = false; + + [[NSUserDefaults standardUserDefaults] setInteger:theEvent.keyCode + forKey:button_to_preference_name(button_being_modified, self.playerListButton.selectedTag)]; + self.controlsTableView.enabled = YES; + self.playerListButton.enabled = YES; + [self.controlsTableView reloadData]; + [self makeFirstResponder:self.controlsTableView]; +} + +- (void) flagsChanged:(NSEvent *)event +{ + if (event.modifierFlags > previousModifiers) { + [self keyDown:event]; + } + + previousModifiers = event.modifierFlags; +} + +- (IBAction)graphicFilterChanged:(NSPopUpButton *)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:[[self class] filterList][[sender indexOfSelectedItem]] + forKey:@"GBFilter"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil]; +} + +- (IBAction)highpassFilterChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) + forKey:@"GBHighpassFilter"]; + [[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 + forKey:@"GBAspectRatioUnkept"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBAspectChanged" object:nil]; +} + +- (IBAction)colorCorrectionChanged:(id)sender +{ + [[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 +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) + forKey:@"GBRewindLength"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRewindLengthChanged" object:nil]; +} + +- (IBAction) configureJoypad:(id)sender +{ + [self.configureJoypadButton setEnabled:NO]; + [self.skipButton setEnabled:YES]; + joystick_being_configured = nil; + [self advanceConfigurationStateMachine]; +} + +- (IBAction) skipButton:(id)sender +{ + [self advanceConfigurationStateMachine]; +} + +- (void) advanceConfigurationStateMachine +{ + 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:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]]; + } + else { + joystick_configuration_state = -1; + [self.configureJoypadButton setEnabled:YES]; + [self.skipButton setEnabled:NO]; + [self.configureJoypadButton setTitle:@"Configure Joypad"]; + } +} + +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button +{ + /* 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 = controller.uniqueID; + } + else if (![joystick_being_configured isEqualToString:controller.uniqueID]) { + return; + } + + NSMutableDictionary *instance_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"] mutableCopy]; + + NSMutableDictionary *name_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"] mutableCopy]; + + + if (!instance_mappings) { + instance_mappings = [[NSMutableDictionary alloc] init]; + } + + if (!name_mappings) { + name_mappings = [[NSMutableDictionary alloc] init]; + } + + NSMutableDictionary *mapping = nil; + if (joystick_configuration_state != 0) { + mapping = [instance_mappings[controller.uniqueID] mutableCopy]; + } + else { + mapping = [[NSMutableDictionary alloc] init]; + } + + + 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, + }; + + 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]; +} + +- (NSButton *)analogControlsCheckbox +{ + return _analogControlsCheckbox; +} + +- (void)setAnalogControlsCheckbox:(NSButton *)analogControlsCheckbox +{ + _analogControlsCheckbox = analogControlsCheckbox; + [_analogControlsCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]]; +} + +- (NSButton *)aspectRatioCheckbox +{ + return _aspectRatioCheckbox; +} + +- (void)setAspectRatioCheckbox:(NSButton *)aspectRatioCheckbox +{ + _aspectRatioCheckbox = aspectRatioCheckbox; + [_aspectRatioCheckbox setState: ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAspectRatioUnkept"]]; +} + +- (void)awakeFromNib +{ + [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]; +} + +- (IBAction)selectOtherBootROMFolder:(id)sender +{ + NSOpenPanel *panel = [[NSOpenPanel alloc] init]; + [panel setCanChooseDirectories:YES]; + [panel setCanChooseFiles:NO]; + [panel setPrompt:@"Select"]; + [panel setDirectoryURL:[[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"]]; + [panel beginSheetModalForWindow:self completionHandler:^(NSModalResponse result) { + if (result == NSModalResponseOK) { + NSURL *url = [[panel URLs] firstObject]; + [[NSUserDefaults standardUserDefaults] setURL:url forKey:@"GBBootROMsFolder"]; + } + [self updateBootROMFolderButton]; + }]; + +} + +- (void) updateBootROMFolderButton +{ + NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"]; + BOOL is_dir = false; + [[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&is_dir]; + if (!is_dir) url = nil; + + if (url) { + [self.bootROMsFolderItem setTitle:[url lastPathComponent]]; + NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:[url path]]; + [icon setSize:NSMakeSize(16, 16)]; + [self.bootROMsFolderItem setHidden:NO]; + [self.bootROMsFolderItem setImage:icon]; + [self.bootROMsButton selectItemAtIndex:1]; + } + else { + [self.bootROMsFolderItem setHidden:YES]; + [self.bootROMsButton selectItemAtIndex:0]; + } +} + +- (IBAction)useBuiltinBootROMs:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setURL:nil forKey:@"GBBootROMsFolder"]; + [self updateBootROMFolderButton]; +} + +- (void)setDmgPopupButton:(NSPopUpButton *)dmgPopupButton +{ + _dmgPopupButton = dmgPopupButton; + [_dmgPopupButton selectItemWithTag:[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"]]; +} + +- (NSPopUpButton *)dmgPopupButton +{ + return _dmgPopupButton; +} + +- (void)setSgbPopupButton:(NSPopUpButton *)sgbPopupButton +{ + _sgbPopupButton = sgbPopupButton; + [_sgbPopupButton selectItemWithTag:[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]]; +} + +- (NSPopUpButton *)sgbPopupButton +{ + return _sgbPopupButton; +} + +- (void)setCgbPopupButton:(NSPopUpButton *)cgbPopupButton +{ + _cgbPopupButton = cgbPopupButton; + [_cgbPopupButton selectItemWithTag:[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]]; +} + +- (NSPopUpButton *)cgbPopupButton +{ + return _cgbPopupButton; +} + +- (IBAction)dmgModelChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) + forKey:@"GBDMGModel"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBDMGModelChanged" object:nil]; + +} + +- (IBAction)sgbModelChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) + forKey:@"GBSGBModel"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBSGBModelChanged" object:nil]; +} + +- (IBAction)cgbModelChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) + forKey:@"GBCGBModel"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBCGBModelChanged" object:nil]; +} + +- (IBAction)reloadButtonsData:(id)sender +{ + [self.controlsTableView reloadData]; +} + +- (void)setPreferredJoypadButton:(NSPopUpButton *)preferredJoypadButton +{ + _preferredJoypadButton = preferredJoypadButton; + [self refreshJoypadMenu:nil]; +} + +- (NSPopUpButton *)preferredJoypadButton +{ + 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 +{ + 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]; + } + } + + 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]; + } + + + if (!selected_controller) { + [self.preferredJoypadButton selectItemWithTitle:@"None"]; + } + [self.controlsTableView reloadData]; +} + +- (IBAction)changeDefaultJoypad:(id)sender +{ + NSMutableDictionary *default_joypads = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"] mutableCopy]; + if (!default_joypads) { + default_joypads = [[NSMutableDictionary alloc] init]; + } + + NSString *player_string = n2s(self.playerListButton.selectedTag); + if ([[sender titleOfSelectedItem] isEqualToString:@"None"]) { + [default_joypads removeObjectForKey:player_string]; + } + else { + default_joypads[player_string] = [[sender selectedItem] identifier]; + } + [[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"JoyKitDefaultControllers"]; +} +@end diff --git a/bsnes/gb/Cocoa/GBSplitView.h b/bsnes/gb/Cocoa/GBSplitView.h new file mode 100644 index 00000000..6ab97cf0 --- /dev/null +++ b/bsnes/gb/Cocoa/GBSplitView.h @@ -0,0 +1,7 @@ +#import + +@interface GBSplitView : NSSplitView + +-(void) setDividerColor:(NSColor *)color; +- (NSArray *)arrangedSubviews; +@end diff --git a/bsnes/gb/Cocoa/GBSplitView.m b/bsnes/gb/Cocoa/GBSplitView.m new file mode 100644 index 00000000..a56c24e6 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h new file mode 100644 index 00000000..484e0c35 --- /dev/null +++ b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h @@ -0,0 +1,6 @@ +#import +#include + +@interface GBTerminalTextFieldCell : NSTextFieldCell +@property GB_gameboy_t *gb; +@end diff --git a/bsnes/gb/Cocoa/GBTerminalTextFieldCell.m b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.m new file mode 100644 index 00000000..c1ed2036 --- /dev/null +++ b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.m @@ -0,0 +1,231 @@ +#import +#import "GBTerminalTextFieldCell.h" + +@interface GBTerminalTextView : NSTextView +@property GB_gameboy_t *gb; +@end + +@implementation GBTerminalTextFieldCell +{ + GBTerminalTextView *field_editor; +} + +- (NSTextView *)fieldEditorForView:(NSView *)controlView +{ + if (field_editor) { + field_editor.gb = self.gb; + return field_editor; + } + field_editor = [[GBTerminalTextView alloc] init]; + [field_editor setFieldEditor:YES]; + field_editor.gb = self.gb; + return field_editor; +} + +@end + +@implementation GBTerminalTextView +{ + NSMutableOrderedSet *lines; + NSUInteger current_line; + bool reverse_search_mode; + NSRange auto_complete_range; + uintptr_t auto_complete_context; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return NULL; + } + lines = [[NSMutableOrderedSet alloc] init]; + return self; +} + +- (void)moveUp:(id)sender +{ + reverse_search_mode = false; + if (current_line != 0) { + current_line--; + [self setString:[lines objectAtIndex:current_line]]; + } + else { + [self setSelectedRange:NSMakeRange(0, 0)]; + NSBeep(); + } +} + +- (void)moveDown:(id)sender +{ + reverse_search_mode = false; + if (current_line == [lines count]) { + [self setString:@""]; + NSBeep(); + return; + } + current_line++; + if (current_line == [lines count]) { + [self setString:@""]; + } + else { + [self setString:[lines objectAtIndex:current_line]]; + } +} + +-(void)insertNewline:(id)sender +{ + if ([self.string length]) { + NSString *string = [self.string copy]; + [lines removeObject:string]; + [lines addObject:string]; + } + [super insertNewline:sender]; + current_line = [lines count]; + reverse_search_mode = false; +} + +- (void)keyDown:(NSEvent *)event +{ + if (event.keyCode == kVK_ANSI_R && (event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagControl) { + if ([lines count] == 0) { + NSBeep(); + return; + } + if (!reverse_search_mode) { + [self selectAll:self]; + current_line = [lines count] - 1; + } + else { + if (current_line != 0) { + current_line--; + } + else { + NSBeep(); + } + } + + if (self.string.length) { + [self updateReverseSearch]; + } + else { + [self setNeedsDisplay:YES]; + reverse_search_mode = true; + } + + } + else { + [super keyDown:event]; + } +} + +- (void) updateReverseSearch +{ + NSUInteger old_line = current_line; + reverse_search_mode = false; + NSString *substring = [self.string substringWithRange:self.selectedRange]; + do { + NSString *line = [lines objectAtIndex:current_line]; + NSRange range = [line rangeOfString:substring]; + if (range.location != NSNotFound) { + self.string = line; + [self setSelectedRange:range]; + reverse_search_mode = true; + return; + } + } while (current_line--); + current_line = old_line; + reverse_search_mode = true; + NSBeep(); +} + +- (void) insertText:(NSString *)string replacementRange:(NSRange)range +{ + if (reverse_search_mode) { + range = self.selectedRange; + self.string = [[self.string substringWithRange:range] stringByAppendingString:string]; + [self selectAll:nil]; + [self updateReverseSearch]; + } + else { + [super insertText:string replacementRange:range]; + } +} + +-(void)deleteBackward:(id)sender +{ + if (reverse_search_mode && self.string.length) { + NSRange range = self.selectedRange; + range.length--; + self.string = [self.string substringWithRange:range]; + if (range.length) { + [self selectAll:nil]; + [self updateReverseSearch]; + } + else { + reverse_search_mode = true; + current_line = [lines count] - 1; + } + } + else { + [super deleteBackward:sender]; + } +} + +-(void)setSelectedRanges:(NSArray *)ranges affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag +{ + reverse_search_mode = false; + auto_complete_context = 0; + [super setSelectedRanges:ranges affinity:affinity stillSelecting:stillSelectingFlag]; +} + +- (BOOL)resignFirstResponder +{ + reverse_search_mode = false; + return [super resignFirstResponder]; +} + +-(void)drawRect:(NSRect)dirtyRect +{ + [super drawRect:dirtyRect]; + if (reverse_search_mode && [super string].length == 0) { + NSMutableDictionary *attributes = [self.typingAttributes mutableCopy]; + NSColor *color = [attributes[NSForegroundColorAttributeName] colorWithAlphaComponent:0.5]; + [attributes setObject:color forKey:NSForegroundColorAttributeName]; + [[[NSAttributedString alloc] initWithString:@"Reverse search..." attributes:attributes] drawAtPoint:NSMakePoint(2, 0)]; + } +} + +/* Todo: lazy design, make it use a delegate instead of having a gb reference*/ + +- (void)insertTab:(id)sender +{ + if (auto_complete_context == 0) { + NSRange selection = self.selectedRange; + if (selection.length) { + [self delete:nil]; + } + auto_complete_range = NSMakeRange(selection.location, 0); + } + char *substring = strdup([self.string substringToIndex:auto_complete_range.location].UTF8String); + uintptr_t context = auto_complete_context; + char *completion = GB_debugger_complete_substring(self.gb, substring, &context); + free(substring); + if (completion) { + NSString *ns_completion = @(completion); + free(completion); + if (!ns_completion) { + goto error; + } + self.selectedRange = auto_complete_range; + auto_complete_range.length = ns_completion.length; + [self replaceCharactersInRange:self.selectedRange withString:ns_completion]; + auto_complete_context = context; + return; + } +error: + auto_complete_context = context; + NSBeep(); +} + +@end diff --git a/bsnes/gb/Cocoa/GBView.h b/bsnes/gb/Cocoa/GBView.h new file mode 100644 index 00000000..80721cd7 --- /dev/null +++ b/bsnes/gb/Cocoa/GBView.h @@ -0,0 +1,26 @@ +#import +#include +#import + +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) GB_frame_blending_mode_t frameBlendingMode; +@property (getter=isMouseHidingEnabled) BOOL mouseHidingEnabled; +@property bool isRewinding; +@property NSView *internalView; +- (void) createInternalView; +- (uint32_t *)currentBuffer; +- (uint32_t *)previousBuffer; +- (void)screenSizeChanged; +- (void)setRumble: (double)amp; +@end diff --git a/bsnes/gb/Cocoa/GBView.m b/bsnes/gb/Cocoa/GBView.m new file mode 100644 index 00000000..6854ba7b --- /dev/null +++ b/bsnes/gb/Cocoa/GBView.m @@ -0,0 +1,553 @@ +#import +#import +#import "GBView.h" +#import "GBViewGL.h" +#import "GBViewMetal.h" +#import "GBButtons.h" +#import "NSString+StringForKey.h" + +#define JOYSTICK_HIGH 0x4000 +#define JOYSTICK_LOW 0x3800 + +static const uint8_t workboy_ascii_to_key[] = { + ['0'] = GB_WORKBOY_0, + ['`'] = GB_WORKBOY_UMLAUT, + ['1'] = GB_WORKBOY_1, + ['2'] = GB_WORKBOY_2, + ['3'] = GB_WORKBOY_3, + ['4'] = GB_WORKBOY_4, + ['5'] = GB_WORKBOY_5, + ['6'] = GB_WORKBOY_6, + ['7'] = GB_WORKBOY_7, + ['8'] = GB_WORKBOY_8, + ['9'] = GB_WORKBOY_9, + + ['\r'] = GB_WORKBOY_ENTER, + [3] = GB_WORKBOY_ENTER, + + ['!'] = GB_WORKBOY_EXCLAMATION_MARK, + ['$'] = GB_WORKBOY_DOLLAR, + ['#'] = GB_WORKBOY_HASH, + ['~'] = GB_WORKBOY_TILDE, + ['*'] = GB_WORKBOY_ASTERISK, + ['+'] = GB_WORKBOY_PLUS, + ['-'] = GB_WORKBOY_MINUS, + ['('] = GB_WORKBOY_LEFT_PARENTHESIS, + [')'] = GB_WORKBOY_RIGHT_PARENTHESIS, + [';'] = GB_WORKBOY_SEMICOLON, + [':'] = GB_WORKBOY_COLON, + ['%'] = GB_WORKBOY_PERCENT, + ['='] = GB_WORKBOY_EQUAL, + [','] = GB_WORKBOY_COMMA, + ['<'] = GB_WORKBOY_LT, + ['.'] = GB_WORKBOY_DOT, + ['>'] = GB_WORKBOY_GT, + ['/'] = GB_WORKBOY_SLASH, + ['?'] = GB_WORKBOY_QUESTION_MARK, + [' '] = GB_WORKBOY_SPACE, + ['\''] = GB_WORKBOY_QUOTE, + ['@'] = GB_WORKBOY_AT, + + ['q'] = GB_WORKBOY_Q, + ['w'] = GB_WORKBOY_W, + ['e'] = GB_WORKBOY_E, + ['r'] = GB_WORKBOY_R, + ['t'] = GB_WORKBOY_T, + ['y'] = GB_WORKBOY_Y, + ['u'] = GB_WORKBOY_U, + ['i'] = GB_WORKBOY_I, + ['o'] = GB_WORKBOY_O, + ['p'] = GB_WORKBOY_P, + ['a'] = GB_WORKBOY_A, + ['s'] = GB_WORKBOY_S, + ['d'] = GB_WORKBOY_D, + ['f'] = GB_WORKBOY_F, + ['g'] = GB_WORKBOY_G, + ['h'] = GB_WORKBOY_H, + ['j'] = GB_WORKBOY_J, + ['k'] = GB_WORKBOY_K, + ['l'] = GB_WORKBOY_L, + ['z'] = GB_WORKBOY_Z, + ['x'] = GB_WORKBOY_X, + ['c'] = GB_WORKBOY_C, + ['v'] = GB_WORKBOY_V, + ['b'] = GB_WORKBOY_B, + ['n'] = GB_WORKBOY_N, + ['m'] = GB_WORKBOY_M, +}; + +static const uint8_t workboy_vk_to_key[] = { + [kVK_F1] = GB_WORKBOY_CLOCK, + [kVK_F2] = GB_WORKBOY_TEMPERATURE, + [kVK_F3] = GB_WORKBOY_MONEY, + [kVK_F4] = GB_WORKBOY_CALCULATOR, + [kVK_F5] = GB_WORKBOY_DATE, + [kVK_F6] = GB_WORKBOY_CONVERSION, + [kVK_F7] = GB_WORKBOY_RECORD, + [kVK_F8] = GB_WORKBOY_WORLD, + [kVK_F9] = GB_WORKBOY_PHONE, + [kVK_F10] = GB_WORKBOY_UNKNOWN, + [kVK_Delete] = GB_WORKBOY_BACKSPACE, + [kVK_Shift] = GB_WORKBOY_SHIFT_DOWN, + [kVK_RightShift] = GB_WORKBOY_SHIFT_DOWN, + [kVK_UpArrow] = GB_WORKBOY_UP, + [kVK_DownArrow] = GB_WORKBOY_DOWN, + [kVK_LeftArrow] = GB_WORKBOY_LEFT, + [kVK_RightArrow] = GB_WORKBOY_RIGHT, + [kVK_Escape] = GB_WORKBOY_ESCAPE, + [kVK_ANSI_KeypadDecimal] = GB_WORKBOY_DECIMAL_POINT, + [kVK_ANSI_KeypadClear] = GB_WORKBOY_M, + [kVK_ANSI_KeypadMultiply] = GB_WORKBOY_H, + [kVK_ANSI_KeypadDivide] = GB_WORKBOY_J, +}; + +@implementation GBView +{ + uint32_t *image_buffers[3]; + unsigned char current_buffer; + BOOL mouse_hidden; + NSTrackingArea *tracking_area; + BOOL _mouseHidingEnabled; + bool axisActive[2]; + bool underclockKeyDown; + double clockMultiplier; + double analogClockMultiplier; + bool analogClockMultiplierValid; + NSEventModifierFlags previousModifiers; + JOYController *lastController; + GB_frame_blending_mode_t _frameBlendingMode; +} + ++ (instancetype)alloc +{ + return [self allocWithZone:NULL]; +} + ++ (instancetype)allocWithZone:(struct _NSZone *)zone +{ + if (self == [GBView class]) { + if ([GBViewMetal isSupported]) { + return [GBViewMetal allocWithZone: zone]; + } + return [GBViewGL allocWithZone: zone]; + } + return [super allocWithZone:zone]; +} + +- (void) createInternalView +{ + assert(false && "createInternalView must not be inherited"); +} + +- (void) _init +{ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ratioKeepingChanged) name:@"GBAspectChanged" object:nil]; + tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){} + options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect + owner:self + userInfo:nil]; + [self addTrackingArea:tracking_area]; + clockMultiplier = 1.0; + [self createInternalView]; + [self addSubview:self.internalView]; + self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [JOYController registerListener:self]; +} + +- (void)screenSizeChanged +{ + if (image_buffers[0]) free(image_buffers[0]); + if (image_buffers[1]) free(image_buffers[1]); + if (image_buffers[2]) free(image_buffers[2]); + + size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(_gb) * GB_get_screen_height(_gb); + + 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]; + }); +} + +- (void) ratioKeepingChanged +{ + [self setFrame:self.superview.frame]; +} + +- (void) setFrameBlendingMode:(GB_frame_blending_mode_t)frameBlendingMode +{ + _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 _frameBlendingMode? 3 : 2; +} + +- (void)dealloc +{ + free(image_buffers[0]); + free(image_buffers[1]); + free(image_buffers[2]); + if (mouse_hidden) { + mouse_hidden = false; + [NSCursor unhide]; + } + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self setRumble:0]; + [JOYController unregisterListener:self]; +} +- (instancetype)initWithCoder:(NSCoder *)coder +{ + if (!(self = [super initWithCoder:coder])) { + return self; + } + [self _init]; + return self; +} + +- (instancetype)initWithFrame:(NSRect)frameRect +{ + if (!(self = [super initWithFrame:frameRect])) { + return self; + } + [self _init]; + return self; +} + +- (void)setFrame:(NSRect)frame +{ + frame = self.superview.frame; + if (_gb && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAspectRatioUnkept"]) { + double ratio = frame.size.width / frame.size.height; + double width = GB_get_screen_width(_gb); + double height = GB_get_screen_height(_gb); + if (ratio >= width / height) { + double new_width = round(frame.size.height / height * width); + frame.origin.x = floor((frame.size.width - new_width) / 2); + frame.size.width = new_width; + frame.origin.y = 0; + } + else { + double new_height = round(frame.size.width / width * height); + frame.origin.y = floor((frame.size.height - new_height) / 2); + frame.size.height = new_height; + frame.origin.x = 0; + } + } + + [super setFrame:frame]; +} + +- (void) flip +{ + if (analogClockMultiplierValid && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]) { + GB_set_clock_multiplier(_gb, analogClockMultiplier); + if (analogClockMultiplier == 1.0) { + analogClockMultiplierValid = false; + } + } + 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; +} + +- (uint32_t *) pixels +{ + return image_buffers[(current_buffer + 1) % self.numberOfBuffers]; +} + +-(void)keyDown:(NSEvent *)theEvent +{ + if ([theEvent type] != NSEventTypeFlagsChanged && theEvent.isARepeat) return; + unsigned short keyCode = theEvent.keyCode; + if (GB_workboy_is_enabled(_gb)) { + if (theEvent.keyCode < sizeof(workboy_vk_to_key) && workboy_vk_to_key[theEvent.keyCode]) { + GB_workboy_set_key(_gb, workboy_vk_to_key[theEvent.keyCode]); + return; + } + unichar c = [theEvent type] != NSEventTypeFlagsChanged? [theEvent.charactersIgnoringModifiers.lowercaseString characterAtIndex:0] : 0; + if (c < sizeof(workboy_ascii_to_key) && workboy_ascii_to_key[c]) { + GB_workboy_set_key(_gb, workboy_ascii_to_key[c]); + return; + } + } + + bool handled = false; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + unsigned player_count = GB_get_player_count(_gb); + for (unsigned player = 0; player < player_count; player++) { + for (GBButton button = 0; button < GBButtonCount; button++) { + NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)]; + if (!key) continue; + + if (key.unsignedShortValue == keyCode) { + handled = true; + switch (button) { + case GBTurbo: + GB_set_turbo_mode(_gb, true, self.isRewinding); + analogClockMultiplierValid = false; + break; + + case GBRewind: + self.isRewinding = true; + GB_set_turbo_mode(_gb, false, false); + break; + + case GBUnderclock: + underclockKeyDown = true; + analogClockMultiplierValid = false; + break; + + default: + GB_set_key_state_for_player(_gb, (GB_key_t)button, player, true); + break; + } + } + } + } + + if (!handled && [theEvent type] != NSEventTypeFlagsChanged) { + [super keyDown:theEvent]; + } +} + +-(void)keyUp:(NSEvent *)theEvent +{ + unsigned short keyCode = theEvent.keyCode; + if (GB_workboy_is_enabled(_gb)) { + if (keyCode == kVK_Shift || keyCode == kVK_RightShift) { + GB_workboy_set_key(_gb, GB_WORKBOY_SHIFT_UP); + } + else { + GB_workboy_set_key(_gb, GB_WORKBOY_NONE); + } + + } + bool handled = false; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + unsigned player_count = GB_get_player_count(_gb); + for (unsigned player = 0; player < player_count; player++) { + for (GBButton button = 0; button < GBButtonCount; button++) { + NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)]; + if (!key) continue; + + if (key.unsignedShortValue == keyCode) { + handled = true; + switch (button) { + case GBTurbo: + GB_set_turbo_mode(_gb, false, false); + analogClockMultiplierValid = false; + break; + + case GBRewind: + self.isRewinding = false; + break; + + case GBUnderclock: + underclockKeyDown = false; + analogClockMultiplierValid = false; + break; + + default: + GB_set_key_state_for_player(_gb, (GB_key_t)button, player, false); + break; + } + } + } + } + if (!handled && [theEvent type] != NSEventTypeFlagsChanged) { + [super keyUp:theEvent]; + } +} + +- (void)setRumble:(double)amp +{ + [lastController setRumbleAmplitude:amp]; +} + +- (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis +{ + if (![self.window isMainWindow]) return; + + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + if (!mapping) { + mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; + } + + if ((axis.usage == JOYAxisUsageR1 && !mapping) || + axis.uniqueID == [mapping[@"AnalogUnderclock"] unsignedLongValue]){ + analogClockMultiplier = MIN(MAX(1 - axis.value + 0.2, 1.0 / 3), 1.0); + analogClockMultiplierValid = true; + } + + else if ((axis.usage == JOYAxisUsageL1 && !mapping) || + axis.uniqueID == [mapping[@"AnalogTurbo"] unsignedLongValue]){ + analogClockMultiplier = MIN(MAX(axis.value * 3 + 0.8, 1.0), 3.0); + analogClockMultiplierValid = true; + } +} + +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button +{ + if (![self.window isMainWindow]) return; + if (controller != lastController) { + [self setRumble:0]; + lastController = controller; + } + + + unsigned player_count = GB_get_player_count(_gb); + + IOPMAssertionID assertionID; + IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); + + for (unsigned player = 0; player < player_count; 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:controller.uniqueID]) { + continue; + } + 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; + } + } +} + +- (BOOL)acceptsFirstResponder +{ + return YES; +} + +- (void)mouseEntered:(NSEvent *)theEvent +{ + if (!mouse_hidden) { + mouse_hidden = true; + if (_mouseHidingEnabled) { + [NSCursor hide]; + } + } + [super mouseEntered:theEvent]; +} + +- (void)mouseExited:(NSEvent *)theEvent +{ + if (mouse_hidden) { + mouse_hidden = false; + if (_mouseHidingEnabled) { + [NSCursor unhide]; + } + } + [super mouseExited:theEvent]; +} + +- (void)setMouseHidingEnabled:(BOOL)mouseHidingEnabled +{ + if (mouseHidingEnabled == _mouseHidingEnabled) return; + + _mouseHidingEnabled = mouseHidingEnabled; + + if (mouse_hidden && _mouseHidingEnabled) { + [NSCursor hide]; + } + + if (mouse_hidden && !_mouseHidingEnabled) { + [NSCursor unhide]; + } +} + +- (BOOL)isMouseHidingEnabled +{ + return _mouseHidingEnabled; +} + +- (void) flagsChanged:(NSEvent *)event +{ + if (event.modifierFlags > previousModifiers) { + [self keyDown:event]; + } + else { + [self keyUp:event]; + } + + previousModifiers = event.modifierFlags; +} + +- (uint32_t *)currentBuffer +{ + return image_buffers[current_buffer]; +} + +- (uint32_t *)previousBuffer +{ + return image_buffers[(current_buffer + 2) % self.numberOfBuffers]; +} + +@end diff --git a/bsnes/gb/Cocoa/GBViewGL.h b/bsnes/gb/Cocoa/GBViewGL.h new file mode 100644 index 00000000..28db4848 --- /dev/null +++ b/bsnes/gb/Cocoa/GBViewGL.h @@ -0,0 +1,5 @@ +#import "GBView.h" + +@interface GBViewGL : GBView + +@end diff --git a/bsnes/gb/Cocoa/GBViewGL.m b/bsnes/gb/Cocoa/GBViewGL.m new file mode 100644 index 00000000..b80973e4 --- /dev/null +++ b/bsnes/gb/Cocoa/GBViewGL.m @@ -0,0 +1,35 @@ +#import "GBViewGL.h" +#import "GBOpenGLView.h" + +@implementation GBViewGL + +- (void)createInternalView +{ + NSOpenGLPixelFormatAttribute attrs[] = + { + NSOpenGLPFAOpenGLProfile, + NSOpenGLProfileVersion3_2Core, + 0 + }; + + NSOpenGLPixelFormat *pf = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; + + assert(pf); + + NSOpenGLContext *context = [[NSOpenGLContext alloc] initWithFormat:pf shareContext:nil]; + + self.internalView = [[GBOpenGLView alloc] initWithFrame:self.frame pixelFormat:pf]; + ((GBOpenGLView *)self.internalView).wantsBestResolutionOpenGLSurface = YES; + ((GBOpenGLView *)self.internalView).openGLContext = context; +} + +- (void)flip +{ + [super flip]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.internalView setNeedsDisplay:YES]; + [self setNeedsDisplay:YES]; + }); +} + +@end diff --git a/bsnes/gb/Cocoa/GBViewMetal.h b/bsnes/gb/Cocoa/GBViewMetal.h new file mode 100644 index 00000000..521c3c72 --- /dev/null +++ b/bsnes/gb/Cocoa/GBViewMetal.h @@ -0,0 +1,7 @@ +#import +#import +#import "GBView.h" + +@interface GBViewMetal : GBView ++ (bool) isSupported; +@end diff --git a/bsnes/gb/Cocoa/GBViewMetal.m b/bsnes/gb/Cocoa/GBViewMetal.m new file mode 100644 index 00000000..9a1c78b6 --- /dev/null +++ b/bsnes/gb/Cocoa/GBViewMetal.m @@ -0,0 +1,215 @@ +#import "GBViewMetal.h" +#pragma clang diagnostic ignored "-Wpartial-availability" + + +static const vector_float2 rect[] = +{ + {-1, -1}, + { 1, -1}, + {-1, 1}, + { 1, 1}, +}; + +@implementation GBViewMetal +{ + id device; + id texture, previous_texture; + id vertices; + id pipeline_state; + id command_queue; + id frame_blending_mode_buffer; + id output_resolution_buffer; + vector_float2 output_resolution; +} + ++ (bool)isSupported +{ + if (MTLCopyAllDevices) { + return [MTLCopyAllDevices() count]; + } + return false; +} + +- (void) allocateTextures +{ + if (!device) return; + + MTLTextureDescriptor *texture_descriptor = [[MTLTextureDescriptor alloc] init]; + + texture_descriptor.pixelFormat = MTLPixelFormatRGBA8Unorm; + + texture_descriptor.width = GB_get_screen_width(self.gb); + texture_descriptor.height = GB_get_screen_height(self.gb); + + texture = [device newTextureWithDescriptor:texture_descriptor]; + previous_texture = [device newTextureWithDescriptor:texture_descriptor]; + +} + +- (void)createInternalView +{ + MTKView *view = [[MTKView alloc] initWithFrame:self.frame device:(device = MTLCreateSystemDefaultDevice())]; + view.delegate = self; + self.internalView = view; + view.paused = YES; + view.enableSetNeedsDisplay = YES; + + vertices = [device newBufferWithBytes:rect + length:sizeof(rect) + 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) + options:MTLResourceStorageModeShared]; + + output_resolution = (simd_float2){view.drawableSize.width, view.drawableSize.height}; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadShader) name:@"GBFilterChanged" object:nil]; + [self loadShader]; +} + +- (void) loadShader +{ + NSError *error = nil; + NSString *shader_source = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"MasterShader" + ofType:@"metal" + inDirectory:@"Shaders"] + encoding:NSUTF8StringEncoding + error:nil]; + + NSString *shader_name = [[NSUserDefaults standardUserDefaults] objectForKey:@"GBFilter"]; + NSString *scaler_source = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:shader_name + ofType:@"fsh" + inDirectory:@"Shaders"] + encoding:NSUTF8StringEncoding + error:nil]; + + shader_source = [shader_source stringByReplacingOccurrencesOfString:@"{filter}" + withString:scaler_source]; + + MTLCompileOptions *options = [[MTLCompileOptions alloc] init]; + options.fastMathEnabled = YES; + id library = [device newLibraryWithSource:shader_source + options:options + error:&error]; + if (error) { + NSLog(@"Error: %@", error); + if (!library) { + return; + } + } + + id vertex_function = [library newFunctionWithName:@"vertex_shader"]; + id fragment_function = [library newFunctionWithName:@"fragment_shader"]; + + // Set up a descriptor for creating a pipeline state object + MTLRenderPipelineDescriptor *pipeline_state_descriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipeline_state_descriptor.vertexFunction = vertex_function; + pipeline_state_descriptor.fragmentFunction = fragment_function; + pipeline_state_descriptor.colorAttachments[0].pixelFormat = ((MTKView *)self.internalView).colorPixelFormat; + + error = nil; + pipeline_state = [device newRenderPipelineStateWithDescriptor:pipeline_state_descriptor + error:&error]; + if (error) { + NSLog(@"Failed to created pipeline state, error %@", error); + return; + } + + command_queue = [device newCommandQueue]; +} + +- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size +{ + output_resolution = (vector_float2){size.width, size.height}; + dispatch_async(dispatch_get_main_queue(), ^{ + [(MTKView *)self.internalView draw]; + }); +} + +- (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]; + } + + MTLRegion region = { + {0, 0, 0}, // MTLOrigin + {texture.width, texture.height, 1} // MTLSize + }; + + [texture replaceRegion:region + mipmapLevel:0 + withBytes:[self currentBuffer] + bytesPerRow:texture.width * 4]; + if ([self frameBlendingMode]) { + [previous_texture replaceRegion:region + mipmapLevel:0 + withBytes:[self previousBuffer] + bytesPerRow:texture.width * 4]; + } + + MTLRenderPassDescriptor *render_pass_descriptor = view.currentRenderPassDescriptor; + id command_buffer = [command_queue commandBuffer]; + + 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 = + [command_buffer renderCommandEncoderWithDescriptor:render_pass_descriptor]; + + [render_encoder setViewport:(MTLViewport){0.0, 0.0, + output_resolution.x, + output_resolution.y, + -1.0, 1.0}]; + + [render_encoder setRenderPipelineState:pipeline_state]; + + [render_encoder setVertexBuffer:vertices + offset:0 + atIndex:0]; + + [render_encoder setFragmentBuffer:frame_blending_mode_buffer + offset:0 + atIndex:0]; + + [render_encoder setFragmentBuffer:output_resolution_buffer + offset:0 + atIndex:1]; + + [render_encoder setFragmentTexture:texture + atIndex:0]; + + [render_encoder setFragmentTexture:previous_texture + atIndex:1]; + + [render_encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip + vertexStart:0 + vertexCount:4]; + + [render_encoder endEncoding]; + + [command_buffer presentDrawable:view.currentDrawable]; + } + + + [command_buffer commit]; +} + +- (void)flip +{ + [super flip]; + dispatch_async(dispatch_get_main_queue(), ^{ + [(MTKView *)self.internalView setNeedsDisplay:YES]; + }); +} + +@end diff --git a/bsnes/gb/Cocoa/GBWarningPopover.h b/bsnes/gb/Cocoa/GBWarningPopover.h new file mode 100644 index 00000000..1d695b13 --- /dev/null +++ b/bsnes/gb/Cocoa/GBWarningPopover.h @@ -0,0 +1,8 @@ +#import + +@interface GBWarningPopover : NSPopover + ++ (GBWarningPopover *) popoverWithContents:(NSString *)contents onView:(NSView *)view; ++ (GBWarningPopover *) popoverWithContents:(NSString *)contents onWindow:(NSWindow *)window; + +@end diff --git a/bsnes/gb/Cocoa/GBWarningPopover.m b/bsnes/gb/Cocoa/GBWarningPopover.m new file mode 100644 index 00000000..411e3883 --- /dev/null +++ b/bsnes/gb/Cocoa/GBWarningPopover.m @@ -0,0 +1,46 @@ +#import "GBWarningPopover.h" + +static GBWarningPopover *lastPopover; + +@implementation GBWarningPopover + ++ (GBWarningPopover *) popoverWithContents:(NSString *)contents onView:(NSView *)view +{ + [lastPopover close]; + lastPopover = [[self alloc] init]; + + [lastPopover setBehavior:NSPopoverBehaviorApplicationDefined]; + [lastPopover setAnimates:YES]; + lastPopover.contentViewController = [[NSViewController alloc] initWithNibName:@"PopoverView" bundle:nil]; + NSTextField *field = (NSTextField *)lastPopover.contentViewController.view; + [field setStringValue:contents]; + NSSize textSize = [field.cell cellSizeForBounds:[field.cell drawingRectForBounds:NSMakeRect(0, 0, 240, CGFLOAT_MAX)]]; + textSize.width = ceil(textSize.width) + 16; + textSize.height = ceil(textSize.height) + 12; + [lastPopover setContentSize:textSize]; + + if (!view.window.isVisible) { + [view.window setIsVisible:YES]; + } + + [lastPopover showRelativeToRect:view.bounds + ofView:view + preferredEdge:NSMinYEdge]; + + NSRect frame = field.frame; + frame.origin.x += 8; + frame.origin.y -= 6; + field.frame = frame; + + + [lastPopover performSelector:@selector(close) withObject:nil afterDelay:3.0]; + + return lastPopover; +} + ++ (GBWarningPopover *)popoverWithContents:(NSString *)contents onWindow:(NSWindow *)window +{ + return [self popoverWithContents:contents onView:window.contentView.superview.subviews.lastObject]; +} + +@end diff --git a/bsnes/gb/Cocoa/Info.plist b/bsnes/gb/Cocoa/Info.plist new file mode 100644 index 00000000..44a21f0f --- /dev/null +++ b/bsnes/gb/Cocoa/Info.plist @@ -0,0 +1,165 @@ + + + + + CFBundleDisplayName + SameBoy + CFBundleDevelopmentRegion + en + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + gb + + CFBundleTypeIconFile + Cartridge + CFBundleTypeName + Game Boy Game + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.github.liji32.sameboy.gb + + LSTypeIsPackage + 0 + NSDocumentClass + Document + + + CFBundleTypeExtensions + + gbc + + CFBundleTypeIconFile + ColorCartridge + CFBundleTypeName + Game Boy Color Game + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.github.liji32.sameboy.gbc + + LSTypeIsPackage + 0 + 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 + CFBundleIconFile + AppIcon.icns + CFBundleIdentifier + com.github.liji32.sameboy + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SameBoy + CFBundlePackageType + APPL + CFBundleShortVersionString + Version @VERSION + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + 10.9 + NSHumanReadableCopyright + Copyright © 2015-2020 Lior Halphon + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy Game + UTTypeIconFile + Cartridge + UTTypeIdentifier + com.github.liji32.sameboy.gb + UTTypeTagSpecification + + public.filename-extension + + gb + + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy Color Game + UTTypeIconFile + ColorCartridge + UTTypeIdentifier + com.github.liji32.sameboy.gbc + UTTypeTagSpecification + + public.filename-extension + + gbc + + + + + 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/bsnes/gb/Cocoa/Joypad.png b/bsnes/gb/Cocoa/Joypad.png new file mode 100644 index 00000000..f30d8f99 Binary files /dev/null and b/bsnes/gb/Cocoa/Joypad.png differ diff --git a/bsnes/gb/Cocoa/Joypad@2x.png b/bsnes/gb/Cocoa/Joypad@2x.png new file mode 100644 index 00000000..d91ee30b Binary files /dev/null and b/bsnes/gb/Cocoa/Joypad@2x.png differ diff --git a/bsnes/gb/Cocoa/Joypad~dark.png b/bsnes/gb/Cocoa/Joypad~dark.png new file mode 100644 index 00000000..8a7687b5 Binary files /dev/null and b/bsnes/gb/Cocoa/Joypad~dark.png differ diff --git a/bsnes/gb/Cocoa/Joypad~dark@2x.png b/bsnes/gb/Cocoa/Joypad~dark@2x.png new file mode 100644 index 00000000..ce2a07cc Binary files /dev/null and b/bsnes/gb/Cocoa/Joypad~dark@2x.png differ diff --git a/bsnes/gb/Cocoa/KeyboardShortcutPrivateAPIs.h b/bsnes/gb/Cocoa/KeyboardShortcutPrivateAPIs.h new file mode 100644 index 00000000..a80dfde9 --- /dev/null +++ b/bsnes/gb/Cocoa/KeyboardShortcutPrivateAPIs.h @@ -0,0 +1,26 @@ +#ifndef KeyboardShortcutPrivateAPIs_h +#define KeyboardShortcutPrivateAPIs_h + +/* These are private APIs, but they are a very simple and comprehensive way + to convert a key equivalent to its display name. */ + +@interface NSKeyboardShortcut : NSObject + ++ (id)shortcutWithPreferencesEncoding:(NSString *)encoding; ++ (id)shortcutWithKeyEquivalent:(NSString *)key_equivalent modifierMask:(unsigned long long)mask; +- (id)initWithKeyEquivalent:(NSString *)key_equivalent modifierMask:(unsigned long long)mask; + +@property(readonly) unsigned long long modifierMask; +@property(readonly) NSString *keyEquivalent; +@property(readonly) NSString *preferencesEncoding; +@property(readonly) NSString *localizedModifierMaskDisplayName; +@property(readonly) NSString *localizedKeyEquivalentDisplayName; +@property(readonly) NSString *localizedDisplayName; + +@end + +@interface NSPrefPaneUtils : NSObject ++ (id)stringForVirtualKey:(unsigned int)key modifiers:(unsigned int)flags; +@end + +#endif \ No newline at end of file diff --git a/bsnes/gb/Cocoa/License.html b/bsnes/gb/Cocoa/License.html new file mode 100644 index 00000000..b21cf8d8 --- /dev/null +++ b/bsnes/gb/Cocoa/License.html @@ -0,0 +1,80 @@ + + + + + + + + + +

SameBoy

+

MIT License

+

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 +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.

+ +

Third-party Libraries

+

HexFiend

+

Copyright © 2005-2009, Peter Ammon +All rights reserved.

+ +

Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met:

+ +
    +
  • Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer.
  • +
  • Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution.
  • +
+ +

THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE

+ + \ No newline at end of file diff --git a/bsnes/gb/Cocoa/MainMenu.xib b/bsnes/gb/Cocoa/MainMenu.xib new file mode 100644 index 00000000..586d872d --- /dev/null +++ b/bsnes/gb/Cocoa/MainMenu.xib @@ -0,0 +1,477 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/NSImageNamedDarkSupport.m b/bsnes/gb/Cocoa/NSImageNamedDarkSupport.m new file mode 100644 index 00000000..821ba3b9 --- /dev/null +++ b/bsnes/gb/Cocoa/NSImageNamedDarkSupport.m @@ -0,0 +1,42 @@ +#import +#import + +@interface NSImageRep(PrivateAPI) +@property(setter=_setAppearanceName:) NSString *_appearanceName; +@end + +static NSImage * (*imageNamed)(Class self, SEL _cmd, NSString *name); + +@implementation NSImage(DarkHooks) + ++ (NSImage *)imageNamedWithDark:(NSImageName)name +{ + NSImage *light = imageNamed(self, _cmd, name); + if (@available(macOS 10.14, *)) { + NSImage *dark = imageNamed(self, _cmd, [name stringByAppendingString:@"~dark"]); + if (!dark) { + return light; + } + NSImage *ret = [[NSImage alloc] initWithSize:light.size]; + for (NSImageRep *rep in light.representations) { + [rep _setAppearanceName:NSAppearanceNameAqua]; + [ret addRepresentation:rep]; + } + for (NSImageRep *rep in dark.representations) { + [rep _setAppearanceName:NSAppearanceNameDarkAqua]; + [ret addRepresentation:rep]; + } + return ret; + } + return light; +} + ++(void)load +{ + if (@available(macOS 10.14, *)) { + imageNamed = (void *)[self methodForSelector:@selector(imageNamed:)]; + method_setImplementation(class_getClassMethod(self, @selector(imageNamed:)), + [self methodForSelector:@selector(imageNamedWithDark:)]); + } +} +@end diff --git a/bsnes/gb/Cocoa/NSObject+MavericksCompat.m b/bsnes/gb/Cocoa/NSObject+MavericksCompat.m new file mode 100644 index 00000000..6c065143 --- /dev/null +++ b/bsnes/gb/Cocoa/NSObject+MavericksCompat.m @@ -0,0 +1,7 @@ +#import +@implementation NSObject (MavericksCompat) +- (instancetype)initWithCoder:(NSCoder *)coder +{ + return [self init]; +} +@end diff --git a/bsnes/gb/Cocoa/NSString+StringForKey.h b/bsnes/gb/Cocoa/NSString+StringForKey.h new file mode 100644 index 00000000..11aabd2d --- /dev/null +++ b/bsnes/gb/Cocoa/NSString+StringForKey.h @@ -0,0 +1,6 @@ +#import + +@interface NSString (StringForKey) ++ (NSString *) displayStringForKeyString: (NSString *)key_string; ++ (NSString *) displayStringForKeyCode:(unsigned short) keyCode; +@end diff --git a/bsnes/gb/Cocoa/NSString+StringForKey.m b/bsnes/gb/Cocoa/NSString+StringForKey.m new file mode 100644 index 00000000..f5a9aa32 --- /dev/null +++ b/bsnes/gb/Cocoa/NSString+StringForKey.m @@ -0,0 +1,46 @@ +#import "NSString+StringForKey.h" +#import "KeyboardShortcutPrivateAPIs.h" +#import + +@implementation NSString (StringForKey) + ++ (NSString *) displayStringForKeyString: (NSString *)key_string +{ + return [[NSKeyboardShortcut shortcutWithKeyEquivalent:key_string modifierMask:0] localizedDisplayName]; +} + ++ (NSString *) displayStringForKeyCode:(unsigned short) keyCode +{ + /* These cases are not handled by stringForVirtualKey */ + switch (keyCode) { + + case kVK_Home: return @"↖"; + case kVK_End: return @"↘"; + case kVK_PageUp: return @"⇞"; + case kVK_PageDown: return @"⇟"; + case kVK_Delete: return @"⌫"; + case kVK_ForwardDelete: return @"⌦"; + case kVK_ANSI_KeypadEnter: return @"⌤"; + case kVK_CapsLock: return @"⇪"; + case kVK_Shift: return @"Left ⇧"; + case kVK_Control: return @"Left ⌃"; + case kVK_Option: return @"Left ⌥"; + case kVK_Command: return @"Left ⌘"; + case kVK_RightShift: return @"Right ⇧"; + case kVK_RightControl: return @"Right ⌃"; + case kVK_RightOption: return @"Right ⌥"; + case kVK_RightCommand: return @"Right ⌘"; + case kVK_Function: return @"fn"; + + /* Label Keypad buttons accordingly */ + default: + if ((keyCode < kVK_ANSI_Keypad0 || keyCode > kVK_ANSI_Keypad9)) { + return [NSPrefPaneUtils stringForVirtualKey:keyCode modifiers:0]; + } + + case kVK_ANSI_KeypadDecimal: case kVK_ANSI_KeypadMultiply: case kVK_ANSI_KeypadPlus: case kVK_ANSI_KeypadDivide: case kVK_ANSI_KeypadMinus: case kVK_ANSI_KeypadEquals: + return [@"Keypad " stringByAppendingString:[NSPrefPaneUtils stringForVirtualKey:keyCode modifiers:0]]; + } +} + +@end diff --git a/bsnes/gb/Cocoa/PkgInfo b/bsnes/gb/Cocoa/PkgInfo new file mode 100644 index 00000000..bd04210f --- /dev/null +++ b/bsnes/gb/Cocoa/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/bsnes/gb/Cocoa/PopoverView.xib b/bsnes/gb/Cocoa/PopoverView.xib new file mode 100644 index 00000000..7ccdf496 --- /dev/null +++ b/bsnes/gb/Cocoa/PopoverView.xib @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/Preferences.xib b/bsnes/gb/Cocoa/Preferences.xib new file mode 100644 index 00000000..99c65435 --- /dev/null +++ b/bsnes/gb/Cocoa/Preferences.xib @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/Speaker.png b/bsnes/gb/Cocoa/Speaker.png new file mode 100644 index 00000000..1f6b556a Binary files /dev/null and b/bsnes/gb/Cocoa/Speaker.png differ diff --git a/bsnes/gb/Cocoa/Speaker@2x.png b/bsnes/gb/Cocoa/Speaker@2x.png new file mode 100644 index 00000000..41c46ffd Binary files /dev/null and b/bsnes/gb/Cocoa/Speaker@2x.png differ diff --git a/bsnes/gb/Cocoa/Speaker~dark.png b/bsnes/gb/Cocoa/Speaker~dark.png new file mode 100644 index 00000000..f3f820a3 Binary files /dev/null and b/bsnes/gb/Cocoa/Speaker~dark.png differ diff --git a/bsnes/gb/Cocoa/Speaker~dark@2x.png b/bsnes/gb/Cocoa/Speaker~dark@2x.png new file mode 100644 index 00000000..bdc3eb70 Binary files /dev/null and b/bsnes/gb/Cocoa/Speaker~dark@2x.png differ diff --git a/bsnes/gb/Cocoa/main.m b/bsnes/gb/Cocoa/main.m new file mode 100644 index 00000000..3eb7a378 --- /dev/null +++ b/bsnes/gb/Cocoa/main.m @@ -0,0 +1,6 @@ +#import + +int main(int argc, const char * argv[]) +{ + return NSApplicationMain(argc, argv); +} diff --git a/bsnes/gb/Core/apu.c b/bsnes/gb/Core/apu.c new file mode 100644 index 00000000..7e7ab31f --- /dev/null +++ b/bsnes/gb/Core/apu.c @@ -0,0 +1,1064 @@ +#include +#include +#include +#include +#include "gb.h" + +#define likely(x) __builtin_expect((x), 1) +#define unlikely(x) __builtin_expect((x), 0) + +static const uint8_t duties[] = { + 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 1, 1, 1, + 0, 1, 1, 1, 1, 1, 1, 0, +}; + +static void refresh_channel(GB_gameboy_t *gb, unsigned index, unsigned cycles_offset) +{ + unsigned multiplier = gb->apu_output.cycles_since_render + cycles_offset - gb->apu_output.last_update[index]; + gb->apu_output.summed_samples[index].left += gb->apu_output.current_sample[index].left * multiplier; + gb->apu_output.summed_samples[index].right += gb->apu_output.current_sample[index].right * multiplier; + gb->apu_output.last_update[index] = gb->apu_output.cycles_since_render + cycles_offset; +} + +bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index) +{ + if (gb->model >= GB_MODEL_AGB) { + /* On the AGB, mixing is done digitally, so there are no per-channel + DACs. Instead, all channels are summed digital regardless of + whatever the DAC state would be on a CGB or earlier model. */ + return true; + } + + switch (index) { + case GB_SQUARE_1: + return gb->io_registers[GB_IO_NR12] & 0xF8; + + case GB_SQUARE_2: + return gb->io_registers[GB_IO_NR22] & 0xF8; + + case GB_WAVE: + return gb->apu.wave_channel.enable; + + case GB_NOISE: + return gb->io_registers[GB_IO_NR42] & 0xF8; + } + + return false; +} + +static uint8_t agb_bias_for_channel(GB_gameboy_t *gb, unsigned index) +{ + if (!gb->apu.is_active[index]) return 0; + + switch (index) { + case GB_SQUARE_1: + return gb->apu.square_channels[GB_SQUARE_1].current_volume; + case GB_SQUARE_2: + return gb->apu.square_channels[GB_SQUARE_2].current_volume; + case GB_WAVE: + return 0; + case GB_NOISE: + return gb->apu.noise_channel.current_volume; + } + return 0; +} + +static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsigned cycles_offset) +{ + if (gb->model >= GB_MODEL_AGB) { + /* On the AGB, because no analog mixing is done, the behavior of NR51 is a bit different. + A channel that is not connected to a terminal is idenitcal to a connected channel + playing PCM sample 0. */ + gb->apu.samples[index] = value; + + if (gb->apu_output.sample_rate) { + unsigned right_volume = (gb->io_registers[GB_IO_NR50] & 7) + 1; + unsigned left_volume = ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1; + + if (index == GB_WAVE) { + /* For some reason, channel 3 is inverted on the AGB */ + value ^= 0xF; + } + + GB_sample_t output; + uint8_t bias = agb_bias_for_channel(gb, index); + + if (gb->io_registers[GB_IO_NR51] & (1 << index)) { + output.right = (0xf - value * 2 + bias) * right_volume; + } + else { + output.right = 0xf * right_volume; + } + + if (gb->io_registers[GB_IO_NR51] & (0x10 << index)) { + output.left = (0xf - value * 2 + bias) * left_volume; + } + else { + output.left = 0xf * left_volume; + } + + if (*(uint32_t *)&(gb->apu_output.current_sample[index]) != *(uint32_t *)&output) { + refresh_channel(gb, index, cycles_offset); + gb->apu_output.current_sample[index] = output; + } + } + + return; + } + + if (!GB_apu_is_DAC_enabled(gb, index)) { + value = gb->apu.samples[index]; + } + else { + gb->apu.samples[index] = value; + } + + if (gb->apu_output.sample_rate) { + unsigned right_volume = 0; + if (gb->io_registers[GB_IO_NR51] & (1 << index)) { + right_volume = (gb->io_registers[GB_IO_NR50] & 7) + 1; + } + unsigned left_volume = 0; + if (gb->io_registers[GB_IO_NR51] & (0x10 << index)) { + left_volume = ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1; + } + GB_sample_t output = {(0xf - value * 2) * left_volume, (0xf - value * 2) * right_volume}; + if (*(uint32_t *)&(gb->apu_output.current_sample[index]) != *(uint32_t *)&output) { + refresh_channel(gb, index, cycles_offset); + gb->apu_output.current_sample[index] = output; + } + } +} + +static double smooth(double x) +{ + return 3*x*x - 2*x*x*x; +} + +static void render(GB_gameboy_t *gb) +{ + GB_sample_t output = {0, 0}; + + UNROLL + for (unsigned i = 0; i < GB_N_CHANNELS; i++) { + double multiplier = CH_STEP; + + if (gb->model < GB_MODEL_AGB) { + if (!GB_apu_is_DAC_enabled(gb, i)) { + gb->apu_output.dac_discharge[i] -= ((double) DAC_DECAY_SPEED) / gb->apu_output.sample_rate; + if (gb->apu_output.dac_discharge[i] < 0) { + multiplier = 0; + gb->apu_output.dac_discharge[i] = 0; + } + else { + multiplier *= smooth(gb->apu_output.dac_discharge[i]); + } + } + else { + gb->apu_output.dac_discharge[i] += ((double) DAC_ATTACK_SPEED) / gb->apu_output.sample_rate; + if (gb->apu_output.dac_discharge[i] > 1) { + gb->apu_output.dac_discharge[i] = 1; + } + else { + multiplier *= smooth(gb->apu_output.dac_discharge[i]); + } + } + } + + if (likely(gb->apu_output.last_update[i] == 0)) { + output.left += gb->apu_output.current_sample[i].left * multiplier; + output.right += gb->apu_output.current_sample[i].right * multiplier; + } + else { + refresh_channel(gb, i, 0); + output.left += (signed long) gb->apu_output.summed_samples[i].left * multiplier + / gb->apu_output.cycles_since_render; + output.right += (signed long) gb->apu_output.summed_samples[i].right * multiplier + / gb->apu_output.cycles_since_render; + gb->apu_output.summed_samples[i] = (GB_sample_t){0, 0}; + } + gb->apu_output.last_update[i] = 0; + } + gb->apu_output.cycles_since_render = 0; + + GB_sample_t filtered_output = gb->apu_output.highpass_mode? + (GB_sample_t) {output.left - gb->apu_output.highpass_diff.left, + output.right - gb->apu_output.highpass_diff.right} : + output; + + switch (gb->apu_output.highpass_mode) { + case GB_HIGHPASS_OFF: + gb->apu_output.highpass_diff = (GB_double_sample_t) {0, 0}; + break; + case GB_HIGHPASS_ACCURATE: + gb->apu_output.highpass_diff = (GB_double_sample_t) + {output.left - filtered_output.left * gb->apu_output.highpass_rate, + output.right - filtered_output.right * gb->apu_output.highpass_rate}; + break; + case GB_HIGHPASS_REMOVE_DC_OFFSET: { + unsigned mask = gb->io_registers[GB_IO_NR51]; + unsigned left_volume = 0; + unsigned right_volume = 0; + UNROLL + for (unsigned i = GB_N_CHANNELS; i--;) { + if (gb->apu.is_active[i]) { + if (mask & 1) { + left_volume += (gb->io_registers[GB_IO_NR50] & 7) * CH_STEP * 0xF; + } + if (mask & 0x10) { + right_volume += ((gb->io_registers[GB_IO_NR50] >> 4) & 7) * CH_STEP * 0xF; + } + } + else { + left_volume += gb->apu_output.current_sample[i].left * CH_STEP; + right_volume += gb->apu_output.current_sample[i].right * CH_STEP; + } + mask >>= 1; + } + gb->apu_output.highpass_diff = (GB_double_sample_t) + {left_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.left * gb->apu_output.highpass_rate, + right_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.right * gb->apu_output.highpass_rate}; + + case GB_HIGHPASS_MAX:; + } + + } + + assert(gb->apu_output.sample_callback); + gb->apu_output.sample_callback(gb, &filtered_output); +} + +static uint16_t new_sweep_sample_length(GB_gameboy_t *gb) +{ + uint16_t delta = gb->apu.shadow_sweep_sample_length >> (gb->io_registers[GB_IO_NR10] & 7); + if (gb->io_registers[GB_IO_NR10] & 8) { + return gb->apu.shadow_sweep_sample_length - delta; + } + return gb->apu.shadow_sweep_sample_length + delta; +} + +static void update_square_sample(GB_gameboy_t *gb, unsigned index) +{ + if (gb->apu.square_channels[index].current_sample_index & 0x80) return; + + uint8_t duty = gb->io_registers[index == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; + update_sample(gb, index, + duties[gb->apu.square_channels[index].current_sample_index + duty * 8]? + gb->apu.square_channels[index].current_volume : 0, + 0); +} + + +/* the effects of NRX2 writes on current volume are not well documented and differ + between models and variants. The exact behavior can only be verified on CGB as it + requires the PCM12 register. The behavior implemented here was verified on *my* + CGB, which might behave differently from other CGB revisions, as well as from the + DMG, MGB or SGB/2 */ +static void nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value) +{ + if (value & 8) { + (*volume)++; + } + + if (((value ^ old_value) & 8)) { + *volume = 0x10 - *volume; + } + + if ((value & 7) && !(old_value & 7) && *volume && !(value & 8)) { + (*volume)--; + } + + if ((old_value & 7) && (value & 8)) { + (*volume)--; + } + + (*volume) &= 0xF; +} + +static void tick_square_envelope(GB_gameboy_t *gb, enum GB_CHANNELS index) +{ + uint8_t nrx2 = gb->io_registers[index == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; + + if (gb->apu.square_channels[index].volume_countdown || (nrx2 & 7)) { + if (!gb->apu.square_channels[index].volume_countdown || !--gb->apu.square_channels[index].volume_countdown) { + if (gb->cgb_double_speed) { + if (index == GB_SQUARE_1) { + gb->apu.pcm_mask[0] &= gb->apu.square_channels[GB_SQUARE_1].current_volume | 0xF1; + } + else { + gb->apu.pcm_mask[0] &= (gb->apu.square_channels[GB_SQUARE_2].current_volume << 2) | 0x1F; + } + } + + if ((nrx2 & 8) && gb->apu.square_channels[index].current_volume < 0xF) { + gb->apu.square_channels[index].current_volume++; + } + + else if (!(nrx2 & 8) && gb->apu.square_channels[index].current_volume > 0) { + gb->apu.square_channels[index].current_volume--; + } + + gb->apu.square_channels[index].volume_countdown = nrx2 & 7; + + if (gb->apu.is_active[index]) { + update_square_sample(gb, index); + } + } + } +} + +static void tick_noise_envelope(GB_gameboy_t *gb) +{ + uint8_t nr42 = gb->io_registers[GB_IO_NR42]; + + if (gb->apu.noise_channel.volume_countdown || (nr42 & 7)) { + if (!gb->apu.noise_channel.volume_countdown || !--gb->apu.noise_channel.volume_countdown) { + if (gb->cgb_double_speed) { + gb->apu.pcm_mask[0] &= (gb->apu.noise_channel.current_volume << 2) | 0x1F; + } + if ((nr42 & 8) && gb->apu.noise_channel.current_volume < 0xF) { + gb->apu.noise_channel.current_volume++; + } + + else if (!(nr42 & 8) && gb->apu.noise_channel.current_volume > 0) { + gb->apu.noise_channel.current_volume--; + } + + gb->apu.noise_channel.volume_countdown = nr42 & 7; + + if (gb->apu.is_active[GB_NOISE]) { + update_sample(gb, GB_NOISE, + (gb->apu.noise_channel.lfsr & 1) ? + gb->apu.noise_channel.current_volume : 0, + 0); + } + } + } +} + +void GB_apu_div_event(GB_gameboy_t *gb) +{ + if (!gb->apu.global_enable) return; + if (gb->apu.skip_div_event == GB_SKIP_DIV_EVENT_SKIP) { + gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_SKIPPED; + return; + } + if (gb->apu.skip_div_event == GB_SKIP_DIV_EVENT_SKIPPED) { + gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_INACTIVE; + } + else { + gb->apu.div_divider++; + } + + if ((gb->apu.div_divider & 1) == 0) { + for (unsigned i = GB_SQUARE_2 + 1; i--;) { + uint8_t nrx2 = gb->io_registers[i == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; + if (gb->apu.is_active[i] && gb->apu.square_channels[i].volume_countdown == 0 && (nrx2 & 7)) { + tick_square_envelope(gb, i); + } + } + + if (gb->apu.is_active[GB_NOISE] && gb->apu.noise_channel.volume_countdown == 0 && (gb->io_registers[GB_IO_NR42] & 7)) { + tick_noise_envelope(gb); + } + } + + if ((gb->apu.div_divider & 7) == 0) { + for (unsigned i = GB_SQUARE_2 + 1; i--;) { + tick_square_envelope(gb, i); + } + + tick_noise_envelope(gb); + } + + if ((gb->apu.div_divider & 1) == 1) { + for (unsigned i = GB_SQUARE_2 + 1; i--;) { + if (gb->apu.square_channels[i].length_enabled) { + if (gb->apu.square_channels[i].pulse_length) { + if (!--gb->apu.square_channels[i].pulse_length) { + gb->apu.is_active[i] = false; + update_sample(gb, i, 0, 0); + } + } + } + } + + if (gb->apu.wave_channel.length_enabled) { + if (gb->apu.wave_channel.pulse_length) { + if (!--gb->apu.wave_channel.pulse_length) { + gb->apu.is_active[GB_WAVE] = false; + update_sample(gb, GB_WAVE, 0, 0); + } + } + } + + if (gb->apu.noise_channel.length_enabled) { + if (gb->apu.noise_channel.pulse_length) { + if (!--gb->apu.noise_channel.pulse_length) { + gb->apu.is_active[GB_NOISE] = false; + update_sample(gb, GB_NOISE, 0, 0); + } + } + } + } + + if ((gb->apu.div_divider & 3) == 3) { + if (!gb->apu.sweep_enabled) { + return; + } + if (gb->apu.square_sweep_countdown) { + if (!--gb->apu.square_sweep_countdown) { + if ((gb->io_registers[GB_IO_NR10] & 0x70) && (gb->io_registers[GB_IO_NR10] & 0x07)) { + gb->apu.square_channels[GB_SQUARE_1].sample_length = + gb->apu.shadow_sweep_sample_length = + gb->apu.new_sweep_sample_length; + } + + if (gb->io_registers[GB_IO_NR10] & 0x70) { + /* Recalculation and overflow check only occurs after a delay */ + gb->apu.square_sweep_calculate_countdown = 0x13 - gb->apu.lf_div; + } + + gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7); + if (!gb->apu.square_sweep_countdown) gb->apu.square_sweep_countdown = 8; + } + } + } +} + + +void GB_apu_run(GB_gameboy_t *gb) +{ + /* Convert 4MHZ to 2MHz. apu_cycles is always divisable by 4. */ + uint8_t cycles = gb->apu.apu_cycles >> 2; + gb->apu.apu_cycles = 0; + if (!cycles) return; + + if (likely(!gb->stopped || GB_is_cgb(gb))) { + /* To align the square signal to 1MHz */ + gb->apu.lf_div ^= cycles & 1; + gb->apu.noise_channel.alignment += cycles; + + if (gb->apu.square_sweep_calculate_countdown) { + if (gb->apu.square_sweep_calculate_countdown > cycles) { + gb->apu.square_sweep_calculate_countdown -= cycles; + } + else { + /* APU bug: sweep frequency is checked after adding the sweep delta twice */ + gb->apu.new_sweep_sample_length = new_sweep_sample_length(gb); + if (gb->apu.new_sweep_sample_length > 0x7ff) { + gb->apu.is_active[GB_SQUARE_1] = false; + update_sample(gb, GB_SQUARE_1, 0, gb->apu.square_sweep_calculate_countdown - cycles); + gb->apu.sweep_enabled = false; + } + gb->apu.sweep_decreasing |= gb->io_registers[GB_IO_NR10] & 8; + gb->apu.square_sweep_calculate_countdown = 0; + } + } + + UNROLL + for (unsigned i = GB_SQUARE_1; i <= GB_SQUARE_2; i++) { + if (gb->apu.is_active[i]) { + uint8_t cycles_left = cycles; + while (unlikely(cycles_left > gb->apu.square_channels[i].sample_countdown)) { + cycles_left -= gb->apu.square_channels[i].sample_countdown + 1; + gb->apu.square_channels[i].sample_countdown = (gb->apu.square_channels[i].sample_length ^ 0x7FF) * 2 + 1; + gb->apu.square_channels[i].current_sample_index++; + gb->apu.square_channels[i].current_sample_index &= 0x7; + if (cycles_left == 0 && gb->apu.samples[i] == 0) { + gb->apu.pcm_mask[0] &= i == GB_SQUARE_1? 0xF0 : 0x0F; + } + + update_square_sample(gb, i); + } + if (cycles_left) { + gb->apu.square_channels[i].sample_countdown -= cycles_left; + } + } + } + + gb->apu.wave_channel.wave_form_just_read = false; + if (gb->apu.is_active[GB_WAVE]) { + uint8_t cycles_left = cycles; + while (unlikely(cycles_left > gb->apu.wave_channel.sample_countdown)) { + cycles_left -= gb->apu.wave_channel.sample_countdown + 1; + gb->apu.wave_channel.sample_countdown = gb->apu.wave_channel.sample_length ^ 0x7FF; + gb->apu.wave_channel.current_sample_index++; + gb->apu.wave_channel.current_sample_index &= 0x1F; + gb->apu.wave_channel.current_sample = + gb->apu.wave_channel.wave_form[gb->apu.wave_channel.current_sample_index]; + update_sample(gb, GB_WAVE, + gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, + cycles - cycles_left); + gb->apu.wave_channel.wave_form_just_read = true; + } + if (cycles_left) { + gb->apu.wave_channel.sample_countdown -= cycles_left; + gb->apu.wave_channel.wave_form_just_read = false; + } + } + + if (gb->apu.is_active[GB_NOISE]) { + uint8_t cycles_left = cycles; + while (unlikely(cycles_left > gb->apu.noise_channel.sample_countdown)) { + cycles_left -= gb->apu.noise_channel.sample_countdown + 1; + gb->apu.noise_channel.sample_countdown = gb->apu.noise_channel.sample_length * 4 + 3; + + /* Step LFSR */ + unsigned high_bit_mask = gb->apu.noise_channel.narrow ? 0x4040 : 0x4000; + bool new_high_bit = (gb->apu.noise_channel.lfsr ^ (gb->apu.noise_channel.lfsr >> 1) ^ 1) & 1; + gb->apu.noise_channel.lfsr >>= 1; + + if (new_high_bit) { + gb->apu.noise_channel.lfsr |= high_bit_mask; + } + else { + /* This code is not redundent, it's relevant when switching LFSR widths */ + gb->apu.noise_channel.lfsr &= ~high_bit_mask; + } + + gb->apu.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; + + if (cycles_left == 0 && gb->apu.samples[GB_NOISE] == 0) { + gb->apu.pcm_mask[1] &= 0x0F; + } + + update_sample(gb, GB_NOISE, + gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + 0); + } + if (cycles_left) { + gb->apu.noise_channel.sample_countdown -= cycles_left; + } + } + } + + if (gb->apu_output.sample_rate) { + gb->apu_output.cycles_since_render += cycles; + + if (gb->apu_output.sample_cycles >= gb->apu_output.cycles_per_sample) { + gb->apu_output.sample_cycles -= gb->apu_output.cycles_per_sample; + render(gb); + } + } +} +void GB_apu_init(GB_gameboy_t *gb) +{ + memset(&gb->apu, 0, sizeof(gb->apu)); + /* Restore the wave form */ + for (unsigned reg = GB_IO_WAV_START; reg <= GB_IO_WAV_END; reg++) { + gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2] = gb->io_registers[reg] >> 4; + gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = gb->io_registers[reg] & 0xF; + } + gb->apu.lf_div = 1; + /* APU glitch: When turning the APU on while DIV's bit 4 (or 5 in double speed mode) is on, + the first DIV/APU event is skipped. */ + if (gb->div_counter & (gb->cgb_double_speed? 0x2000 : 0x1000)) { + gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_SKIP; + gb->apu.div_divider = 1; + } +} + +uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg) +{ + if (reg == GB_IO_NR52) { + uint8_t value = 0; + for (unsigned i = 0; i < GB_N_CHANNELS; i++) { + value >>= 1; + if (gb->apu.is_active[i]) { + value |= 0x8; + } + } + if (gb->apu.global_enable) { + value |= 0x80; + } + value |= 0x70; + return value; + } + + static const char read_mask[GB_IO_WAV_END - GB_IO_NR10 + 1] = { + /* NRX0 NRX1 NRX2 NRX3 NRX4 */ + 0x80, 0x3F, 0x00, 0xFF, 0xBF, // NR1X + 0xFF, 0x3F, 0x00, 0xFF, 0xBF, // NR2X + 0x7F, 0xFF, 0x9F, 0xFF, 0xBF, // NR3X + 0xFF, 0xFF, 0x00, 0x00, 0xBF, // NR4X + 0x00, 0x00, 0x70, 0xFF, 0xFF, // NR5X + + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Unused + // Wave RAM + 0, /* ... */ + }; + + if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END && gb->apu.is_active[GB_WAVE]) { + if (!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) { + return 0xFF; + } + reg = GB_IO_WAV_START + gb->apu.wave_channel.current_sample_index / 2; + } + + return gb->io_registers[reg] | read_mask[reg - GB_IO_NR10]; +} + +void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) +{ + if (!gb->apu.global_enable && reg != GB_IO_NR52 && reg < GB_IO_WAV_START && (GB_is_cgb(gb) || + ( + reg != GB_IO_NR11 && + reg != GB_IO_NR21 && + reg != GB_IO_NR31 && + reg != GB_IO_NR41 + ) + )) { + return; + } + + if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END && gb->apu.is_active[GB_WAVE]) { + if (!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) { + return; + } + reg = GB_IO_WAV_START + gb->apu.wave_channel.current_sample_index / 2; + } + + /* Todo: this can and should be rewritten with a function table. */ + switch (reg) { + /* Globals */ + case GB_IO_NR50: + case GB_IO_NR51: + gb->io_registers[reg] = value; + /* These registers affect the output of all 4 channels (but not the output of the PCM registers).*/ + /* We call update_samples with the current value so the APU output is updated with the new outputs */ + for (unsigned i = GB_N_CHANNELS; i--;) { + update_sample(gb, i, gb->apu.samples[i], 0); + } + break; + case GB_IO_NR52: { + + uint8_t old_nrx1[] = { + gb->io_registers[GB_IO_NR11], + gb->io_registers[GB_IO_NR21], + gb->io_registers[GB_IO_NR31], + gb->io_registers[GB_IO_NR41] + }; + if ((value & 0x80) && !gb->apu.global_enable) { + GB_apu_init(gb); + gb->apu.global_enable = true; + } + else if (!(value & 0x80) && gb->apu.global_enable) { + for (unsigned i = GB_N_CHANNELS; i--;) { + update_sample(gb, i, 0, 0); + } + memset(&gb->apu, 0, sizeof(gb->apu)); + memset(gb->io_registers + GB_IO_NR10, 0, GB_IO_WAV_START - GB_IO_NR10); + old_nrx1[0] &= 0x3F; + old_nrx1[1] &= 0x3F; + + gb->apu.global_enable = false; + } + + if (!GB_is_cgb(gb) && (value & 0x80)) { + GB_apu_write(gb, GB_IO_NR11, old_nrx1[0]); + GB_apu_write(gb, GB_IO_NR21, old_nrx1[1]); + GB_apu_write(gb, GB_IO_NR31, old_nrx1[2]); + GB_apu_write(gb, GB_IO_NR41, old_nrx1[3]); + } + } + break; + + /* Square channels */ + case GB_IO_NR10: + if (gb->apu.sweep_decreasing && !(value & 8)) { + gb->apu.is_active[GB_SQUARE_1] = false; + update_sample(gb, GB_SQUARE_1, 0, 0); + gb->apu.sweep_enabled = false; + gb->apu.square_sweep_calculate_countdown = 0; + } + if ((value & 0x70) == 0) { + /* Todo: what happens if we set period to 0 while a calculate event is scheduled, and then + re-set it to non-zero? */ + gb->apu.square_sweep_calculate_countdown = 0; + } + break; + + case GB_IO_NR11: + case GB_IO_NR21: { + unsigned index = reg == GB_IO_NR21? GB_SQUARE_2: GB_SQUARE_1; + gb->apu.square_channels[index].pulse_length = (0x40 - (value & 0x3f)); + if (!gb->apu.global_enable) { + value &= 0x3f; + } + break; + } + + case GB_IO_NR12: + case GB_IO_NR22: { + unsigned index = reg == GB_IO_NR22? GB_SQUARE_2: GB_SQUARE_1; + if (((value & 0x7) == 0) && ((gb->io_registers[reg] & 0x7) != 0)) { + /* Envelope disabled */ + gb->apu.square_channels[index].volume_countdown = 0; + } + if ((value & 0xF8) == 0) { + /* This disables the DAC */ + gb->io_registers[reg] = value; + gb->apu.is_active[index] = false; + update_sample(gb, index, 0, 0); + } + else if (gb->apu.is_active[index]) { + nrx2_glitch(&gb->apu.square_channels[index].current_volume, value, gb->io_registers[reg]); + update_square_sample(gb, index); + } + + break; + } + + case GB_IO_NR13: + case GB_IO_NR23: { + unsigned index = reg == GB_IO_NR23? GB_SQUARE_2: GB_SQUARE_1; + gb->apu.square_channels[index].sample_length &= ~0xFF; + gb->apu.square_channels[index].sample_length |= value & 0xFF; + break; + } + + case GB_IO_NR14: + case GB_IO_NR24: { + unsigned index = reg == GB_IO_NR24? GB_SQUARE_2: GB_SQUARE_1; + + /* TODO: When the sample length changes right before being updated, the countdown should change to the + old length, but the current sample should not change. Because our write timing isn't accurate to + the T-cycle, we hack around it by stepping the sample index backwards. */ + if ((value & 0x80) == 0 && gb->apu.is_active[index]) { + /* On an AGB, as well as on CGB C and earlier (TODO: Tested: 0, B and C), it behaves slightly different on + double speed. */ + if (gb->model == GB_MODEL_CGB_E /* || gb->model == GB_MODEL_CGB_D */ || gb->apu.square_channels[index].sample_countdown & 1) { + if (gb->apu.square_channels[index].sample_countdown >> 1 == (gb->apu.square_channels[index].sample_length ^ 0x7FF)) { + gb->apu.square_channels[index].current_sample_index--; + gb->apu.square_channels[index].current_sample_index &= 7; + } + } + } + + gb->apu.square_channels[index].sample_length &= 0xFF; + gb->apu.square_channels[index].sample_length |= (value & 7) << 8; + if (index == GB_SQUARE_1) { + gb->apu.shadow_sweep_sample_length = + gb->apu.new_sweep_sample_length = + gb->apu.square_channels[0].sample_length; + } + if (value & 0x80) { + /* Current sample index remains unchanged when restarting channels 1 or 2. It is only reset by + turning the APU off. */ + if (!gb->apu.is_active[index]) { + gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 6 - gb->apu.lf_div; + } + else { + /* Timing quirk: if already active, sound starts 2 (2MHz) ticks earlier.*/ + gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 4 - gb->apu.lf_div; + } + gb->apu.square_channels[index].current_volume = gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] >> 4; + + /* The volume changes caused by NRX4 sound start take effect instantly (i.e. the effect the previously + started sound). The playback itself is not instant which is why we don't update the sample for other + cases. */ + if (gb->apu.is_active[index]) { + update_square_sample(gb, index); + } + + gb->apu.square_channels[index].volume_countdown = gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] & 7; + + if ((gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] & 0xF8) != 0 && !gb->apu.is_active[index]) { + gb->apu.is_active[index] = true; + update_sample(gb, index, 0, 0); + /* We use the highest bit in current_sample_index to mark this sample is not actually playing yet, */ + gb->apu.square_channels[index].current_sample_index |= 0x80; + } + if (gb->apu.square_channels[index].pulse_length == 0) { + gb->apu.square_channels[index].pulse_length = 0x40; + gb->apu.square_channels[index].length_enabled = false; + } + + if (index == GB_SQUARE_1) { + gb->apu.sweep_decreasing = false; + if (gb->io_registers[GB_IO_NR10] & 7) { + /* APU bug: if shift is nonzero, overflow check also occurs on trigger */ + gb->apu.square_sweep_calculate_countdown = 0x13 - gb->apu.lf_div; + } + else { + gb->apu.square_sweep_calculate_countdown = 0; + } + gb->apu.sweep_enabled = gb->io_registers[GB_IO_NR10] & 0x77; + gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7); + if (!gb->apu.square_sweep_countdown) gb->apu.square_sweep_countdown = 8; + } + + } + + /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ + if ((value & 0x40) && + !gb->apu.square_channels[index].length_enabled && + (gb->apu.div_divider & 1) && + gb->apu.square_channels[index].pulse_length) { + gb->apu.square_channels[index].pulse_length--; + if (gb->apu.square_channels[index].pulse_length == 0) { + if (value & 0x80) { + gb->apu.square_channels[index].pulse_length = 0x3F; + } + else { + gb->apu.is_active[index] = false; + update_sample(gb, index, 0, 0); + } + } + } + gb->apu.square_channels[index].length_enabled = value & 0x40; + break; + } + + /* Wave channel */ + case GB_IO_NR30: + gb->apu.wave_channel.enable = value & 0x80; + if (!gb->apu.wave_channel.enable) { + gb->apu.is_active[GB_WAVE] = false; + update_sample(gb, GB_WAVE, 0, 0); + } + break; + case GB_IO_NR31: + gb->apu.wave_channel.pulse_length = (0x100 - value); + break; + case GB_IO_NR32: + gb->apu.wave_channel.shift = (uint8_t[]){4, 0, 1, 2}[(value >> 5) & 3]; + if (gb->apu.is_active[GB_WAVE]) { + update_sample(gb, GB_WAVE, gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, 0); + } + break; + case GB_IO_NR33: + gb->apu.wave_channel.sample_length &= ~0xFF; + gb->apu.wave_channel.sample_length |= value & 0xFF; + break; + case GB_IO_NR34: + gb->apu.wave_channel.sample_length &= 0xFF; + gb->apu.wave_channel.sample_length |= (value & 7) << 8; + if ((value & 0x80)) { + /* DMG bug: wave RAM gets corrupted if the channel is retriggerred 1 cycle before the APU + reads from it. */ + if (!GB_is_cgb(gb) && + gb->apu.is_active[GB_WAVE] && + gb->apu.wave_channel.sample_countdown == 0 && + gb->apu.wave_channel.enable) { + unsigned offset = ((gb->apu.wave_channel.current_sample_index + 1) >> 1) & 0xF; + + /* This glitch varies between models and even specific instances: + DMG-B: Most of them behave as emulated. A few behave differently. + SGB: As far as I know, all tested instances behave as emulated. + MGB, SGB2: Most instances behave non-deterministically, a few behave as emulated. + + Additionally, I believe DMGs, including those we behave differently than emulated, + are all deterministic. */ + if (offset < 4) { + gb->io_registers[GB_IO_WAV_START] = gb->io_registers[GB_IO_WAV_START + offset]; + gb->apu.wave_channel.wave_form[0] = gb->apu.wave_channel.wave_form[offset / 2]; + gb->apu.wave_channel.wave_form[1] = gb->apu.wave_channel.wave_form[offset / 2 + 1]; + } + else { + memcpy(gb->io_registers + GB_IO_WAV_START, + gb->io_registers + GB_IO_WAV_START + (offset & ~3), + 4); + memcpy(gb->apu.wave_channel.wave_form, + gb->apu.wave_channel.wave_form + (offset & ~3) * 2, + 8); + } + } + if (!gb->apu.is_active[GB_WAVE]) { + gb->apu.is_active[GB_WAVE] = true; + update_sample(gb, GB_WAVE, + gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, + 0); + } + gb->apu.wave_channel.sample_countdown = (gb->apu.wave_channel.sample_length ^ 0x7FF) + 3; + gb->apu.wave_channel.current_sample_index = 0; + if (gb->apu.wave_channel.pulse_length == 0) { + gb->apu.wave_channel.pulse_length = 0x100; + gb->apu.wave_channel.length_enabled = false; + } + /* Note that we don't change the sample just yet! This was verified on hardware. */ + } + + /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ + if ((value & 0x40) && + !gb->apu.wave_channel.length_enabled && + (gb->apu.div_divider & 1) && + gb->apu.wave_channel.pulse_length) { + gb->apu.wave_channel.pulse_length--; + if (gb->apu.wave_channel.pulse_length == 0) { + if (value & 0x80) { + gb->apu.wave_channel.pulse_length = 0xFF; + } + else { + gb->apu.is_active[GB_WAVE] = false; + update_sample(gb, GB_WAVE, 0, 0); + } + } + } + gb->apu.wave_channel.length_enabled = value & 0x40; + if (gb->apu.is_active[GB_WAVE] && !gb->apu.wave_channel.enable) { + gb->apu.is_active[GB_WAVE] = false; + update_sample(gb, GB_WAVE, 0, 0); + } + + break; + + /* Noise Channel */ + + case GB_IO_NR41: { + gb->apu.noise_channel.pulse_length = (0x40 - (value & 0x3f)); + break; + } + + case GB_IO_NR42: { + if (((value & 0x7) == 0) && ((gb->io_registers[reg] & 0x7) != 0)) { + /* Envelope disabled */ + gb->apu.noise_channel.volume_countdown = 0; + } + if ((value & 0xF8) == 0) { + /* This disables the DAC */ + gb->io_registers[reg] = value; + gb->apu.is_active[GB_NOISE] = false; + update_sample(gb, GB_NOISE, 0, 0); + } + else if (gb->apu.is_active[GB_NOISE]) { + nrx2_glitch(&gb->apu.noise_channel.current_volume, value, gb->io_registers[reg]); + update_sample(gb, GB_NOISE, + gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + 0); + } + break; + } + + case GB_IO_NR43: { + gb->apu.noise_channel.narrow = value & 8; + unsigned divisor = (value & 0x07) << 1; + if (!divisor) divisor = 1; + gb->apu.noise_channel.sample_length = (divisor << (value >> 4)) - 1; + + /* Todo: changing the frequency sometimes delays the next sample. This is probably + due to how the frequency is actually calculated in the noise channel, which is probably + not by calculating the effective sample length and counting simiarly to the other channels. + This is not emulated correctly. */ + break; + } + + case GB_IO_NR44: { + if (value & 0x80) { + gb->apu.noise_channel.sample_countdown = (gb->apu.noise_channel.sample_length) * 2 + 6 - gb->apu.lf_div; + + /* I'm COMPLETELY unsure about this logic, but it passes all relevant tests. + See comment in NR43. */ + if ((gb->io_registers[GB_IO_NR43] & 7) && (gb->apu.noise_channel.alignment & 2) == 0) { + if ((gb->io_registers[GB_IO_NR43] & 7) == 1) { + gb->apu.noise_channel.sample_countdown += 2; + } + else { + gb->apu.noise_channel.sample_countdown -= 2; + } + } + if (gb->apu.is_active[GB_NOISE]) { + gb->apu.noise_channel.sample_countdown += 2; + } + + gb->apu.noise_channel.current_volume = gb->io_registers[GB_IO_NR42] >> 4; + + /* The volume changes caused by NRX4 sound start take effect instantly (i.e. the effect the previously + started sound). The playback itself is not instant which is why we don't update the sample for other + cases. */ + if (gb->apu.is_active[GB_NOISE]) { + update_sample(gb, GB_NOISE, + gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + 0); + } + gb->apu.noise_channel.lfsr = 0; + gb->apu.current_lfsr_sample = false; + gb->apu.noise_channel.volume_countdown = gb->io_registers[GB_IO_NR42] & 7; + + if (!gb->apu.is_active[GB_NOISE] && (gb->io_registers[GB_IO_NR42] & 0xF8) != 0) { + gb->apu.is_active[GB_NOISE] = true; + update_sample(gb, GB_NOISE, 0, 0); + } + + if (gb->apu.noise_channel.pulse_length == 0) { + gb->apu.noise_channel.pulse_length = 0x40; + gb->apu.noise_channel.length_enabled = false; + } + } + + /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ + if ((value & 0x40) && + !gb->apu.noise_channel.length_enabled && + (gb->apu.div_divider & 1) && + gb->apu.noise_channel.pulse_length) { + gb->apu.noise_channel.pulse_length--; + if (gb->apu.noise_channel.pulse_length == 0) { + if (value & 0x80) { + gb->apu.noise_channel.pulse_length = 0x3F; + } + else { + gb->apu.is_active[GB_NOISE] = false; + update_sample(gb, GB_NOISE, 0, 0); + } + } + } + gb->apu.noise_channel.length_enabled = value & 0x40; + break; + } + + default: + if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END) { + gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2] = value >> 4; + gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = value & 0xF; + } + } + gb->io_registers[reg] = value; +} + +void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate) +{ + + gb->apu_output.sample_rate = sample_rate; + if (sample_rate) { + gb->apu_output.highpass_rate = pow(0.999958, GB_get_clock_rate(gb) / (double)sample_rate); + } + gb->apu_output.rate_set_in_clocks = false; + GB_apu_update_cycles_per_sample(gb); +} + +void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample) +{ + + if (cycles_per_sample == 0) { + GB_set_sample_rate(gb, 0); + return; + } + gb->apu_output.cycles_per_sample = cycles_per_sample; + gb->apu_output.sample_rate = GB_get_clock_rate(gb) / cycles_per_sample * 2; + gb->apu_output.highpass_rate = pow(0.999958, cycles_per_sample); + gb->apu_output.rate_set_in_clocks = true; +} + +void GB_apu_set_sample_callback(GB_gameboy_t *gb, GB_sample_callback_t callback) +{ + gb->apu_output.sample_callback = callback; +} + +void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode) +{ + gb->apu_output.highpass_mode = mode; +} + +void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb) +{ + if (gb->apu_output.rate_set_in_clocks) return; + if (gb->apu_output.sample_rate) { + gb->apu_output.cycles_per_sample = 2 * GB_get_clock_rate(gb) / (double)gb->apu_output.sample_rate; /* 2 * because we use 8MHz units */ + } +} diff --git a/bsnes/gb/Core/apu.h b/bsnes/gb/Core/apu.h new file mode 100644 index 00000000..a3a36a63 --- /dev/null +++ b/bsnes/gb/Core/apu.h @@ -0,0 +1,169 @@ +#ifndef apu_h +#define apu_h +#include +#include +#include +#include "gb_struct_def.h" + + +#ifdef GB_INTERNAL +/* Speed = 1 / Length (in seconds) */ +#define DAC_DECAY_SPEED 20000 +#define DAC_ATTACK_SPEED 20000 + + +/* Divides nicely and never overflows with 4 channels and 8 (1-8) volume levels */ +#ifdef WIIU +/* Todo: Remove this hack once https://github.com/libretro/RetroArch/issues/6252 is fixed*/ +#define MAX_CH_AMP (0xFF0 / 2) +#else +#define MAX_CH_AMP 0xFF0 +#endif +#define CH_STEP (MAX_CH_AMP/0xF/8) +#endif + + + +/* APU ticks are 2MHz, triggered by an internal APU clock. */ + +typedef struct +{ + int16_t left; + int16_t right; +} GB_sample_t; + +typedef struct +{ + double left; + double right; +} GB_double_sample_t; + +enum GB_CHANNELS { + GB_SQUARE_1, + GB_SQUARE_2, + GB_WAVE, + GB_NOISE, + GB_N_CHANNELS +}; + +typedef void (*GB_sample_callback_t)(GB_gameboy_t *gb, GB_sample_t *sample); + +typedef struct +{ + bool global_enable; + uint8_t apu_cycles; + + uint8_t samples[GB_N_CHANNELS]; + bool is_active[GB_N_CHANNELS]; + + uint8_t div_divider; // The DIV register ticks the APU at 512Hz, but is then divided + // once more to generate 128Hz and 64Hz clocks + + uint8_t lf_div; // The APU runs in 2MHz, but channels 1, 2 and 4 run in 1MHZ so we divide + // need to divide the signal. + + uint8_t square_sweep_countdown; // In 128Hz + uint8_t square_sweep_calculate_countdown; // In 2 MHz + uint16_t new_sweep_sample_length; + uint16_t shadow_sweep_sample_length; + bool sweep_enabled; + bool sweep_decreasing; + + struct { + uint16_t pulse_length; // Reloaded from NRX1 (xorred), in 256Hz DIV ticks + uint8_t current_volume; // Reloaded from NRX2 + uint8_t volume_countdown; // Reloaded from NRX2 + uint8_t current_sample_index; /* For save state compatibility, + highest bit is reused (See NR14/NR24's + write code)*/ + + uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length, xorred $7FF) + uint16_t sample_length; // From NRX3, NRX4, in APU ticks + bool length_enabled; // NRX4 + + } square_channels[2]; + + struct { + bool enable; // NR30 + uint16_t pulse_length; // Reloaded from NR31 (xorred), in 256Hz DIV ticks + uint8_t shift; // NR32 + uint16_t sample_length; // NR33, NR34, in APU ticks + bool length_enabled; // NR34 + + uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length, xorred $7FF) + uint8_t current_sample_index; + uint8_t current_sample; // Current sample before shifting. + + int8_t wave_form[32]; + bool wave_form_just_read; + } wave_channel; + + struct { + uint16_t pulse_length; // Reloaded from NR41 (xorred), in 256Hz DIV ticks + uint8_t current_volume; // Reloaded from NR42 + uint8_t volume_countdown; // Reloaded from NR42 + uint16_t lfsr; + bool narrow; + + uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length) + uint16_t sample_length; // From NR43, in APU ticks + bool length_enabled; // NR44 + + uint8_t alignment; // If (NR43 & 7) != 0, samples are aligned to 512KHz clock instead of + // 1MHz. This variable keeps track of the alignment. + + } noise_channel; + +#define GB_SKIP_DIV_EVENT_INACTIVE 0 +#define GB_SKIP_DIV_EVENT_SKIPPED 1 +#define GB_SKIP_DIV_EVENT_SKIP 2 + uint8_t skip_div_event; + bool current_lfsr_sample; + uint8_t pcm_mask[2]; // For CGB-0 to CGB-C PCM read glitch +} GB_apu_t; + +typedef enum { + GB_HIGHPASS_OFF, // Do not apply any filter, keep DC offset + GB_HIGHPASS_ACCURATE, // Apply a highpass filter similar to the one used on hardware + GB_HIGHPASS_REMOVE_DC_OFFSET, // Remove DC Offset without affecting the waveform + GB_HIGHPASS_MAX +} GB_highpass_mode_t; + +typedef struct { + unsigned sample_rate; + + double sample_cycles; // In 8 MHz units + double cycles_per_sample; + + // Samples are NOT normalized to MAX_CH_AMP * 4 at this stage! + unsigned cycles_since_render; + unsigned last_update[GB_N_CHANNELS]; + GB_sample_t current_sample[GB_N_CHANNELS]; + GB_sample_t summed_samples[GB_N_CHANNELS]; + double dac_discharge[GB_N_CHANNELS]; + + GB_highpass_mode_t highpass_mode; + double highpass_rate; + GB_double_sample_t highpass_diff; + + GB_sample_callback_t sample_callback; + + bool rate_set_in_clocks; +} GB_apu_output_t; + +void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate); +void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample); /* Cycles are in 8MHz units */ +void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode); +void GB_apu_set_sample_callback(GB_gameboy_t *gb, GB_sample_callback_t callback); +#ifdef GB_INTERNAL +bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index); +void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value); +uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg); +void GB_apu_div_event(GB_gameboy_t *gb); +void GB_apu_init(GB_gameboy_t *gb); +void GB_apu_run(GB_gameboy_t *gb); +void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb); +void GB_borrow_sgb_border(GB_gameboy_t *gb); +#endif + +#endif /* apu_h */ diff --git a/bsnes/gb/Core/camera.c b/bsnes/gb/Core/camera.c new file mode 100644 index 00000000..bef84890 --- /dev/null +++ b/bsnes/gb/Core/camera.c @@ -0,0 +1,149 @@ +#include "gb.h" + +static signed noise_seed = 0; + +/* This is not a complete emulation of the camera chip. Only the features used by the GameBoy Camera ROMs are supported. + We also do not emulate the timing of the real cart, as it might be actually faster than the webcam. */ + +static uint8_t generate_noise(uint8_t x, uint8_t y) +{ + signed value = (x + y * 128 + noise_seed); + uint8_t *data = (uint8_t *) &value; + unsigned hash = 0; + + while ((signed *) data != &value + 1) { + hash ^= (*data << 8); + if (hash & 0x8000) { + hash ^= 0x8a00; + hash ^= *data; + } + data++; + hash <<= 1; + } + return (hash >> 8); +} + +static long get_processed_color(GB_gameboy_t *gb, uint8_t x, uint8_t y) +{ + if (x >= 128) { + x = 0; + } + if (y >= 112) { + y = 0; + } + + long color = gb->camera_get_pixel_callback? gb->camera_get_pixel_callback(gb, x, y) : (generate_noise(x, y)); + + static const double gain_values[] = + {0.8809390, 0.9149149, 0.9457498, 0.9739758, + 1.0000000, 1.0241412, 1.0466537, 1.0677433, + 1.0875793, 1.1240310, 1.1568911, 1.1868043, + 1.2142561, 1.2396208, 1.2743837, 1.3157323, + 1.3525190, 1.3856512, 1.4157897, 1.4434309, + 1.4689574, 1.4926697, 1.5148087, 1.5355703, + 1.5551159, 1.5735801, 1.5910762, 1.6077008, + 1.6235366, 1.6386550, 1.6531183, 1.6669808}; + /* Multiply color by gain value */ + color *= gain_values[gb->camera_registers[GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS] & 0x1F]; + + + /* Color is multiplied by the exposure register to simulate exposure. */ + color = color * ((gb->camera_registers[GB_CAMERA_EXPOSURE_HIGH] << 8) + gb->camera_registers[GB_CAMERA_EXPOSURE_LOW]) / 0x1000; + + return color; +} + +uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr) +{ + if (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) { + /* Forbid reading the image while the camera is busy. */ + return 0xFF; + } + uint8_t tile_x = addr / 0x10 % 0x10; + uint8_t tile_y = addr / 0x10 / 0x10; + + uint8_t y = ((addr >> 1) & 0x7) + tile_y * 8; + uint8_t bit = addr & 1; + + uint8_t ret = 0; + + for (uint8_t x = tile_x * 8; x < tile_x * 8 + 8; x++) { + + long color = get_processed_color(gb, x, y); + + static const double edge_enhancement_ratios[] = {0.5, 0.75, 1, 1.25, 2, 3, 4, 5}; + double edge_enhancement_ratio = edge_enhancement_ratios[(gb->camera_registers[GB_CAMERA_EDGE_ENHANCEMENT_INVERT_AND_VOLTAGE] >> 4) & 0x7]; + if ((gb->camera_registers[GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS] & 0xE0) == 0xE0) { + color += (color * 4) * edge_enhancement_ratio; + color -= get_processed_color(gb, x - 1, y) * edge_enhancement_ratio; + color -= get_processed_color(gb, x + 1, y) * edge_enhancement_ratio; + color -= get_processed_color(gb, x, y - 1) * edge_enhancement_ratio; + color -= get_processed_color(gb, x, y + 1) * edge_enhancement_ratio; + } + + + /* The camera's registers are used as a threshold pattern, which defines the dithering */ + uint8_t pattern_base = ((x & 3) + (y & 3) * 4) * 3 + GB_CAMERA_DITHERING_PATTERN_START; + + if (color < gb->camera_registers[pattern_base]) { + color = 3; + } + else if (color < gb->camera_registers[pattern_base + 1]) { + color = 2; + } + else if (color < gb->camera_registers[pattern_base + 2]) { + color = 1; + } + else { + color = 0; + } + + ret <<= 1; + ret |= (color >> bit) & 1; + } + + return ret; +} + +void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_callback_t callback) +{ + gb->camera_get_pixel_callback = callback; +} + +void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback) +{ + gb->camera_update_request_callback = callback; +} + +void GB_camera_updated(GB_gameboy_t *gb) +{ + gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] &= ~1; +} + +void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + addr &= 0x7F; + if (addr == GB_CAMERA_SHOOT_AND_1D_FLAGS) { + value &= 0x7; + noise_seed = rand(); + if ((value & 1) && !(gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) && gb->camera_update_request_callback) { + /* If no callback is set, ignore the write as if the camera is instantly done */ + gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] |= 1; + gb->camera_update_request_callback(gb); + } + } + else { + if (addr >= 0x36) { + GB_log(gb, "Wrote invalid camera register %02x: %2x\n", addr, value); + return; + } + gb->camera_registers[addr] = value; + } +} +uint8_t GB_camera_read_register(GB_gameboy_t *gb, uint16_t addr) +{ + if ((addr & 0x7F) == 0) { + return gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS]; + } + return 0; +} diff --git a/bsnes/gb/Core/camera.h b/bsnes/gb/Core/camera.h new file mode 100644 index 00000000..21c69b68 --- /dev/null +++ b/bsnes/gb/Core/camera.h @@ -0,0 +1,29 @@ +#ifndef camera_h +#define camera_h +#include +#include "gb_struct_def.h" + +typedef uint8_t (*GB_camera_get_pixel_callback_t)(GB_gameboy_t *gb, uint8_t x, uint8_t y); +typedef void (*GB_camera_update_request_callback_t)(GB_gameboy_t *gb); + +enum { + GB_CAMERA_SHOOT_AND_1D_FLAGS = 0, + GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS = 1, + GB_CAMERA_EXPOSURE_HIGH = 2, + GB_CAMERA_EXPOSURE_LOW = 3, + GB_CAMERA_EDGE_ENHANCEMENT_INVERT_AND_VOLTAGE = 4, + GB_CAMERA_DITHERING_PATTERN_START = 6, + GB_CAMERA_DITHERING_PATTERN_END = 0x35, +}; + +uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr); + +void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_callback_t callback); +void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback); + +void GB_camera_updated(GB_gameboy_t *gb); + +void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +uint8_t GB_camera_read_register(GB_gameboy_t *gb, uint16_t addr); + +#endif diff --git a/bsnes/gb/Core/cheats.c b/bsnes/gb/Core/cheats.c new file mode 100644 index 00000000..14875e01 --- /dev/null +++ b/bsnes/gb/Core/cheats.c @@ -0,0 +1,313 @@ +#include "gb.h" +#include "cheats.h" +#include +#include +#include + +static inline uint8_t hash_addr(uint16_t addr) +{ + return addr; +} + +static uint16_t bank_for_addr(GB_gameboy_t *gb, uint16_t addr) +{ + if (addr < 0x4000) { + return gb->mbc_rom0_bank; + } + + if (addr < 0x8000) { + return gb->mbc_rom_bank; + } + + if (addr < 0xD000) { + return 0; + } + + if (addr < 0xE000) { + return gb->cgb_ram_bank; + } + + return 0; +} + +void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value) +{ + if (!gb->cheat_enabled) return; + if (!gb->boot_rom_finished) return; + const GB_cheat_hash_t *hash = gb->cheat_hash[hash_addr(address)]; + if (hash) { + for (unsigned i = 0; i < hash->size; i++) { + GB_cheat_t *cheat = hash->cheats[i]; + if (cheat->address == address && cheat->enabled && (!cheat->use_old_value || cheat->old_value == *value)) { + if (cheat->bank == GB_CHEAT_ANY_BANK || cheat->bank == bank_for_addr(gb, address)) { + *value = cheat->value; + break; + } + } + } + } +} + +bool GB_cheats_enabled(GB_gameboy_t *gb) +{ + return gb->cheat_enabled; +} + +void GB_set_cheats_enabled(GB_gameboy_t *gb, bool enabled) +{ + gb->cheat_enabled = enabled; +} + +void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled) +{ + GB_cheat_t *cheat = malloc(sizeof(*cheat)); + cheat->address = address; + cheat->bank = bank; + cheat->value = value; + cheat->old_value = old_value; + cheat->use_old_value = use_old_value; + cheat->enabled = enabled; + strncpy(cheat->description, description, sizeof(cheat->description)); + cheat->description[sizeof(cheat->description) - 1] = 0; + gb->cheats = realloc(gb->cheats, (++gb->cheat_count) * sizeof(*cheat)); + gb->cheats[gb->cheat_count - 1] = cheat; + + GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(address)]; + if (!*hash) { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat)); + (*hash)->size = 1; + (*hash)->cheats[0] = cheat; + } + else { + (*hash)->size++; + *hash = realloc(*hash, sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + (*hash)->cheats[(*hash)->size - 1] = cheat; + } +} + +const GB_cheat_t *const *GB_get_cheats(GB_gameboy_t *gb, size_t *size) +{ + *size = gb->cheat_count; + return (void *)gb->cheats; +} +void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat) +{ + for (unsigned i = 0; i < gb->cheat_count; i++) { + if (gb->cheats[i] == cheat) { + gb->cheats[i] = gb->cheats[--gb->cheat_count]; + if (gb->cheat_count == 0) { + free(gb->cheats); + gb->cheats = NULL; + } + else { + gb->cheats = realloc(gb->cheats, gb->cheat_count * sizeof(*cheat)); + } + break; + } + } + + GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(cheat->address)]; + for (unsigned i = 0; i < (*hash)->size; i++) { + if ((*hash)->cheats[i] == cheat) { + (*hash)->cheats[i] = (*hash)->cheats[(*hash)->size--]; + if ((*hash)->size == 0) { + free(*hash); + *hash = NULL; + } + else { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + } + break; + } + } + + free((void *)cheat); +} + +bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled) +{ + uint8_t dummy; + /* GameShark */ + { + uint8_t bank; + uint8_t value; + uint16_t address; + if (sscanf(cheat, "%02hhx%02hhx%04hx%c", &bank, &value, &address, &dummy) == 3) { + if (bank >= 0x80) { + bank &= 0xF; + } + GB_add_cheat(gb, description, address, bank, value, 0, false, enabled); + return true; + } + } + + /* GameGenie */ + { + char stripped_cheat[10] = {0,}; + for (unsigned i = 0; i < 9 && *cheat; i++) { + stripped_cheat[i] = *(cheat++); + while (*cheat == '-') { + cheat++; + } + } + + // Delete the 7th character; + stripped_cheat[7] = stripped_cheat[8]; + stripped_cheat[8] = 0; + + uint8_t old_value; + uint8_t value; + uint16_t address; + if (sscanf(stripped_cheat, "%02hhx%04hx%02hhx%c", &value, &address, &old_value, &dummy) == 3) { + address = (uint16_t)(address >> 4) | (uint16_t)(address << 12); + address ^= 0xF000; + if (address > 0x7FFF) { + return false; + } + old_value = (uint8_t)(old_value >> 2) | (uint8_t)(old_value << 6); + old_value ^= 0xBA; + GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, old_value, true, enabled); + return true; + } + + if (sscanf(stripped_cheat, "%02hhx%04hx%c", &value, &address, &dummy) == 2) { + address = (uint16_t)(address >> 4) | (uint16_t)(address << 12); + address ^= 0xF000; + if (address > 0x7FFF) { + return false; + } + GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, false, true, enabled); + return true; + } + } + return false; +} + +void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled) +{ + GB_cheat_t *cheat = NULL; + for (unsigned i = 0; i < gb->cheat_count; i++) { + if (gb->cheats[i] == _cheat) { + cheat = gb->cheats[i]; + break; + } + } + + assert(cheat); + + if (cheat->address != address) { + /* Remove from old bucket */ + GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(cheat->address)]; + for (unsigned i = 0; i < (*hash)->size; i++) { + if ((*hash)->cheats[i] == cheat) { + (*hash)->cheats[i] = (*hash)->cheats[(*hash)->size--]; + if ((*hash)->size == 0) { + free(*hash); + *hash = NULL; + } + else { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + } + break; + } + } + cheat->address = address; + + /* Add to new bucket */ + hash = &gb->cheat_hash[hash_addr(address)]; + if (!*hash) { + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat)); + (*hash)->size = 1; + (*hash)->cheats[0] = cheat; + } + else { + (*hash)->size++; + *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + (*hash)->cheats[(*hash)->size - 1] = cheat; + } + } + cheat->bank = bank; + cheat->value = value; + cheat->old_value = old_value; + cheat->use_old_value = use_old_value; + cheat->enabled = enabled; + if (description != cheat->description) { + strncpy(cheat->description, description, sizeof(cheat->description)); + cheat->description[sizeof(cheat->description) - 1] = 0; + } +} + +#define CHEAT_MAGIC 'SBCh' + +void GB_load_cheats(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + return; + } + + uint32_t magic = 0; + uint32_t struct_size = 0; + fread(&magic, sizeof(magic), 1, f); + fread(&struct_size, sizeof(struct_size), 1, f); + if (magic != CHEAT_MAGIC && magic != __builtin_bswap32(CHEAT_MAGIC)) { + GB_log(gb, "The file is not a SameBoy cheat database"); + return; + } + + if (struct_size != sizeof(GB_cheat_t)) { + GB_log(gb, "This cheat database is not compatible with this version of SameBoy"); + return; + } + + // Remove all cheats first + while (gb->cheats) { + GB_remove_cheat(gb, gb->cheats[0]); + } + + GB_cheat_t cheat; + while (fread(&cheat, sizeof(cheat), 1, f)) { + if (magic == __builtin_bswap32(CHEAT_MAGIC)) { + cheat.address = __builtin_bswap16(cheat.address); + cheat.bank = __builtin_bswap16(cheat.bank); + } + cheat.description[sizeof(cheat.description) - 1] = 0; + GB_add_cheat(gb, cheat.description, cheat.address, cheat.bank, cheat.value, cheat.old_value, cheat.use_old_value, cheat.enabled); + } + + return; +} + +int GB_save_cheats(GB_gameboy_t *gb, const char *path) +{ + if (!gb->cheat_count) return 0; // Nothing to save. + FILE *f = fopen(path, "wb"); + if (!f) { + GB_log(gb, "Could not dump cheat database: %s.\n", strerror(errno)); + return errno; + } + + uint32_t magic = CHEAT_MAGIC; + uint32_t struct_size = sizeof(GB_cheat_t); + + if (fwrite(&magic, sizeof(magic), 1, f) != 1) { + fclose(f); + return EIO; + } + + if (fwrite(&struct_size, sizeof(struct_size), 1, f) != 1) { + fclose(f); + return EIO; + } + + for (size_t i = 0; i cheat_count; i++) { + if (fwrite(gb->cheats[i], sizeof(*gb->cheats[i]), 1, f) != 1) { + fclose(f); + return EIO; + } + } + + errno = 0; + fclose(f); + return errno; +} diff --git a/bsnes/gb/Core/cheats.h b/bsnes/gb/Core/cheats.h new file mode 100644 index 00000000..cf8aa20d --- /dev/null +++ b/bsnes/gb/Core/cheats.h @@ -0,0 +1,42 @@ +#ifndef cheats_h +#define cheats_h +#include "gb_struct_def.h" + +#define GB_CHEAT_ANY_BANK 0xFFFF + +typedef struct GB_cheat_s GB_cheat_t; + +void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled); +void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled); +bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled); +const GB_cheat_t *const *GB_get_cheats(GB_gameboy_t *gb, size_t *size); +void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat); +bool GB_cheats_enabled(GB_gameboy_t *gb); +void GB_set_cheats_enabled(GB_gameboy_t *gb, bool enabled); +void GB_load_cheats(GB_gameboy_t *gb, const char *path); +int GB_save_cheats(GB_gameboy_t *gb, const char *path); + +#ifdef GB_INTERNAL +#ifdef GB_DISABLE_CHEATS +#define GB_apply_cheat(...) +#else +void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value); +#endif +#endif + +typedef struct { + size_t size; + GB_cheat_t *cheats[]; +} GB_cheat_hash_t; + +struct GB_cheat_s { + uint16_t address; + uint16_t bank; + uint8_t value; + uint8_t old_value; + bool use_old_value; + bool enabled; + char description[128]; +}; + +#endif diff --git a/bsnes/gb/Core/debugger.c b/bsnes/gb/Core/debugger.c new file mode 100644 index 00000000..002d4554 --- /dev/null +++ b/bsnes/gb/Core/debugger.c @@ -0,0 +1,2668 @@ +#include +#include +#include +#include "gb.h" + +typedef struct { + bool has_bank; + uint16_t bank:9; + uint16_t value; +} value_t; + +typedef struct { + enum { + LVALUE_MEMORY, + LVALUE_MEMORY16, + LVALUE_REG16, + LVALUE_REG_H, + LVALUE_REG_L, + } kind; + union { + uint16_t *register_address; + value_t memory_address; + }; +} lvalue_t; + +#define VALUE_16(x) ((value_t){false, 0, (x)}) + +struct GB_breakpoint_s { + union { + struct { + uint16_t addr; + uint16_t bank; /* -1 = any bank*/ + }; + uint32_t key; /* For sorting and comparing */ + }; + char *condition; + bool is_jump_to; +}; + +#define BP_KEY(x) (((struct GB_breakpoint_s){.addr = ((x).value), .bank = (x).has_bank? (x).bank : -1 }).key) + +#define GB_WATCHPOINT_R (1) +#define GB_WATCHPOINT_W (2) + +struct GB_watchpoint_s { + union { + struct { + uint16_t addr; + uint16_t bank; /* -1 = any bank*/ + }; + uint32_t key; /* For sorting and comparing */ + }; + char *condition; + uint8_t flags; +}; + +#define WP_KEY(x) (((struct GB_watchpoint_s){.addr = ((x).value), .bank = (x).has_bank? (x).bank : -1 }).key) + +static uint16_t bank_for_addr(GB_gameboy_t *gb, uint16_t addr) +{ + if (addr < 0x4000) { + return gb->mbc_rom0_bank; + } + + if (addr < 0x8000) { + return gb->mbc_rom_bank; + } + + if (addr < 0xD000) { + return 0; + } + + if (addr < 0xE000) { + return gb->cgb_ram_bank; + } + + return 0; +} + +typedef struct { + uint16_t rom0_bank; + uint16_t rom_bank; + uint8_t mbc_ram_bank; + bool mbc_ram_enable; + uint8_t ram_bank; + uint8_t vram_bank; +} banking_state_t; + +static inline void save_banking_state(GB_gameboy_t *gb, banking_state_t *state) +{ + state->rom0_bank = gb->mbc_rom0_bank; + state->rom_bank = gb->mbc_rom_bank; + state->mbc_ram_bank = gb->mbc_ram_bank; + state->mbc_ram_enable = gb->mbc_ram_enable; + state->ram_bank = gb->cgb_ram_bank; + state->vram_bank = gb->cgb_vram_bank; +} + +static inline void restore_banking_state(GB_gameboy_t *gb, banking_state_t *state) +{ + + gb->mbc_rom0_bank = state->rom0_bank; + gb->mbc_rom_bank = state->rom_bank; + gb->mbc_ram_bank = state->mbc_ram_bank; + gb->mbc_ram_enable = state->mbc_ram_enable; + gb->cgb_ram_bank = state->ram_bank; + gb->cgb_vram_bank = state->vram_bank; +} + +static inline void switch_banking_state(GB_gameboy_t *gb, uint16_t bank) +{ + gb->mbc_rom0_bank = bank; + gb->mbc_rom_bank = bank; + gb->mbc_ram_bank = bank; + gb->mbc_ram_enable = true; + if (GB_is_cgb(gb)) { + gb->cgb_ram_bank = bank & 7; + gb->cgb_vram_bank = bank & 1; + if (gb->cgb_ram_bank == 0) { + gb->cgb_ram_bank = 1; + } + } +} + +static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer_name) +{ + static __thread char output[256]; + const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, value); + + if (symbol && (value - symbol->addr > 0x1000 || symbol->addr == 0) ) { + symbol = NULL; + } + + /* Avoid overflow */ + if (symbol && strlen(symbol->name) >= 240) { + symbol = NULL; + } + + if (!symbol) { + sprintf(output, "$%04x", value); + } + + else if (symbol->addr == value) { + if (prefer_name) { + sprintf(output, "%s ($%04x)", symbol->name, value); + } + else { + sprintf(output, "$%04x (%s)", value, symbol->name); + } + } + + else { + if (prefer_name) { + sprintf(output, "%s+$%03x ($%04x)", symbol->name, value - symbol->addr, value); + } + else { + sprintf(output, "$%04x (%s+$%03x)", value, symbol->name, value - symbol->addr); + } + } + return output; +} + +static const char *debugger_value_to_string(GB_gameboy_t *gb, value_t value, bool prefer_name) +{ + if (!value.has_bank) return value_to_string(gb, value.value, prefer_name); + + static __thread char output[256]; + const GB_bank_symbol_t *symbol = GB_map_find_symbol(gb->bank_symbols[value.bank], value.value); + + if (symbol && (value.value - symbol->addr > 0x1000 || symbol->addr == 0) ) { + symbol = NULL; + } + + /* Avoid overflow */ + if (symbol && strlen(symbol->name) >= 240) { + symbol = NULL; + } + + if (!symbol) { + sprintf(output, "$%02x:$%04x", value.bank, value.value); + } + + else if (symbol->addr == value.value) { + if (prefer_name) { + sprintf(output, "%s ($%02x:$%04x)", symbol->name, value.bank, value.value); + } + else { + sprintf(output, "$%02x:$%04x (%s)", value.bank, value.value, symbol->name); + } + } + + else { + if (prefer_name) { + sprintf(output, "%s+$%03x ($%02x:$%04x)", symbol->name, value.value - symbol->addr, value.bank, value.value); + } + else { + sprintf(output, "$%02x:$%04x (%s+$%03x)", value.bank, value.value, symbol->name, value.value - symbol->addr); + } + } + return output; +} + +static value_t read_lvalue(GB_gameboy_t *gb, lvalue_t lvalue) +{ + /* Not used until we add support for operators like += */ + switch (lvalue.kind) { + case LVALUE_MEMORY: + if (lvalue.memory_address.has_bank) { + banking_state_t state; + save_banking_state(gb, &state); + switch_banking_state(gb, lvalue.memory_address.bank); + value_t r = VALUE_16(GB_read_memory(gb, lvalue.memory_address.value)); + restore_banking_state(gb, &state); + return r; + } + return VALUE_16(GB_read_memory(gb, lvalue.memory_address.value)); + + case LVALUE_MEMORY16: + if (lvalue.memory_address.has_bank) { + banking_state_t state; + save_banking_state(gb, &state); + switch_banking_state(gb, lvalue.memory_address.bank); + value_t r = VALUE_16(GB_read_memory(gb, lvalue.memory_address.value) | + (GB_read_memory(gb, lvalue.memory_address.value + 1) * 0x100)); + restore_banking_state(gb, &state); + return r; + } + return VALUE_16(GB_read_memory(gb, lvalue.memory_address.value) | + (GB_read_memory(gb, lvalue.memory_address.value + 1) * 0x100)); + + case LVALUE_REG16: + return VALUE_16(*lvalue.register_address); + + case LVALUE_REG_L: + return VALUE_16(*lvalue.register_address & 0x00FF); + + case LVALUE_REG_H: + return VALUE_16(*lvalue.register_address >> 8); + } + + return VALUE_16(0); +} + +static void write_lvalue(GB_gameboy_t *gb, lvalue_t lvalue, uint16_t value) +{ + switch (lvalue.kind) { + case LVALUE_MEMORY: + if (lvalue.memory_address.has_bank) { + banking_state_t state; + save_banking_state(gb, &state); + switch_banking_state(gb, lvalue.memory_address.bank); + GB_write_memory(gb, lvalue.memory_address.value, value); + restore_banking_state(gb, &state); + return; + } + GB_write_memory(gb, lvalue.memory_address.value, value); + return; + + case LVALUE_MEMORY16: + if (lvalue.memory_address.has_bank) { + banking_state_t state; + save_banking_state(gb, &state); + switch_banking_state(gb, lvalue.memory_address.bank); + GB_write_memory(gb, lvalue.memory_address.value, value); + GB_write_memory(gb, lvalue.memory_address.value + 1, value >> 8); + restore_banking_state(gb, &state); + return; + } + GB_write_memory(gb, lvalue.memory_address.value, value); + GB_write_memory(gb, lvalue.memory_address.value + 1, value >> 8); + return; + + case LVALUE_REG16: + *lvalue.register_address = value; + return; + + case LVALUE_REG_L: + *lvalue.register_address &= 0xFF00; + *lvalue.register_address |= value & 0xFF; + return; + + case LVALUE_REG_H: + *lvalue.register_address &= 0x00FF; + *lvalue.register_address |= value << 8; + return; + } +} + +/* 16 bit value 16 bit value = 16 bit value + 25 bit address 16 bit value = 25 bit address + 16 bit value 25 bit address = 25 bit address + 25 bit address 25 bit address = 16 bit value (since adding pointers, for examples, makes no sense) + + Boolean operators always return a 16-bit value + */ +#define FIX_BANK(x) ((value_t){a.has_bank ^ b.has_bank, a.has_bank? a.bank : b.bank, (x)}) + +static value_t add(value_t a, value_t b) {return FIX_BANK(a.value + b.value);} +static value_t sub(value_t a, value_t b) {return FIX_BANK(a.value - b.value);} +static value_t mul(value_t a, value_t b) {return FIX_BANK(a.value * b.value);} +static value_t _div(value_t a, value_t b) +{ + if (b.value == 0) { + return FIX_BANK(0); + } + return FIX_BANK(a.value / b.value); +}; +static value_t mod(value_t a, value_t b) +{ + if (b.value == 0) { + return FIX_BANK(0); + } + return FIX_BANK(a.value % b.value); +}; +static value_t and(value_t a, value_t b) {return FIX_BANK(a.value & b.value);} +static value_t or(value_t a, value_t b) {return FIX_BANK(a.value | b.value);} +static value_t xor(value_t a, value_t b) {return FIX_BANK(a.value ^ b.value);} +static value_t shleft(value_t a, value_t b) {return FIX_BANK(a.value << b.value);} +static value_t shright(value_t a, value_t b) {return FIX_BANK(a.value >> b.value);} +static value_t assign(GB_gameboy_t *gb, lvalue_t a, uint16_t b) +{ + write_lvalue(gb, a, b); + return read_lvalue(gb, a); +} + +static value_t bool_and(value_t a, value_t b) {return VALUE_16(a.value && b.value);} +static value_t bool_or(value_t a, value_t b) {return VALUE_16(a.value || b.value);} +static value_t equals(value_t a, value_t b) {return VALUE_16(a.value == b.value);} +static value_t different(value_t a, value_t b) {return VALUE_16(a.value != b.value);} +static value_t lower(value_t a, value_t b) {return VALUE_16(a.value < b.value);} +static value_t greater(value_t a, value_t b) {return VALUE_16(a.value > b.value);} +static value_t lower_equals(value_t a, value_t b) {return VALUE_16(a.value <= b.value);} +static value_t greater_equals(value_t a, value_t b) {return VALUE_16(a.value >= b.value);} +static value_t bank(value_t a, value_t b) {return (value_t) {true, a.value, b.value};} + + +static struct { + const char *string; + int8_t priority; + value_t (*operator)(value_t, value_t); + value_t (*lvalue_operator)(GB_gameboy_t *, lvalue_t, uint16_t); +} operators[] = +{ + // Yes. This is not C-like. But it makes much more sense. + // Deal with it. + {"+", 0, add}, + {"-", 0, sub}, + {"||", 0, bool_or}, + {"|", 0, or}, + {"*", 1, mul}, + {"/", 1, _div}, + {"%", 1, mod}, + {"&&", 1, bool_and}, + {"&", 1, and}, + {"^", 1, xor}, + {"<<", 2, shleft}, + {"<=", 3, lower_equals}, + {"<", 3, lower}, + {">>", 2, shright}, + {">=", 3, greater_equals}, + {">", 3, greater}, + {"==", 3, equals}, + {"=", -1, NULL, assign}, + {"!=", 3, different}, + {":", 4, bank}, +}; + +value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, + size_t length, bool *error, + uint16_t *watchpoint_address, uint8_t *watchpoint_new_value); + +static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, + size_t length, bool *error, + uint16_t *watchpoint_address, uint8_t *watchpoint_new_value) +{ + *error = false; + // Strip whitespace + while (length && (string[0] == ' ' || string[0] == '\n' || string[0] == '\r' || string[0] == '\t')) { + string++; + length--; + } + while (length && (string[length-1] == ' ' || string[length-1] == '\n' || string[length-1] == '\r' || string[length-1] == '\t')) { + length--; + } + if (length == 0) { + GB_log(gb, "Expected expression.\n"); + *error = true; + return (lvalue_t){0,}; + } + if (string[0] == '(' && string[length - 1] == ')') { + // Attempt to strip parentheses + signed depth = 0; + for (unsigned i = 0; i < length; i++) { + if (string[i] == '(') depth++; + if (depth == 0) { + // First and last are not matching + depth = 1; + break; + } + if (string[i] == ')') depth--; + } + if (depth == 0) return debugger_evaluate_lvalue(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + } + else if (string[0] == '[' && string[length - 1] == ']') { + // Attempt to strip square parentheses (memory dereference) + signed depth = 0; + for (unsigned i = 0; i < length; i++) { + if (string[i] == '[') depth++; + if (depth == 0) { + // First and last are not matching + depth = 1; + break; + } + if (string[i] == ']') depth--; + } + if (depth == 0) { + return (lvalue_t){LVALUE_MEMORY, .memory_address = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value)}; + } + } + else if (string[0] == '{' && string[length - 1] == '}') { + // Attempt to strip curly parentheses (memory dereference) + signed depth = 0; + for (unsigned i = 0; i < length; i++) { + if (string[i] == '{') depth++; + if (depth == 0) { + // First and last are not matching + depth = 1; + break; + } + if (string[i] == '}') depth--; + } + if (depth == 0) { + return (lvalue_t){LVALUE_MEMORY16, .memory_address = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value)}; + } + } + + // Registers + if (string[0] != '$' && (string[0] < '0' || string[0] > '9')) { + if (length == 1) { + switch (string[0]) { + case 'a': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_AF]}; + case 'f': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_AF]}; + case 'b': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_BC]}; + case 'c': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_BC]}; + case 'd': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_DE]}; + case 'e': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_DE]}; + case 'h': return (lvalue_t){LVALUE_REG_H, .register_address = &gb->registers[GB_REGISTER_HL]}; + case 'l': return (lvalue_t){LVALUE_REG_L, .register_address = &gb->registers[GB_REGISTER_HL]}; + } + } + else if (length == 2) { + switch (string[0]) { + case 'a': if (string[1] == 'f') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_AF]}; + case 'b': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_BC]}; + case 'd': if (string[1] == 'e') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_DE]}; + case 'h': if (string[1] == 'l') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_HL]}; + case 's': if (string[1] == 'p') return (lvalue_t){LVALUE_REG16, .register_address = &gb->registers[GB_REGISTER_SP]}; + case 'p': if (string[1] == 'c') return (lvalue_t){LVALUE_REG16, .register_address = &gb->pc}; + } + } + GB_log(gb, "Unknown register: %.*s\n", (unsigned) length, string); + *error = true; + return (lvalue_t){0,}; + } + + GB_log(gb, "Expression is not an lvalue: %.*s\n", (unsigned) length, string); + *error = true; + return (lvalue_t){0,}; +} + +#define ERROR ((value_t){0,}) +value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, + size_t length, bool *error, + uint16_t *watchpoint_address, uint8_t *watchpoint_new_value) +{ + /* Disable watchpoints while evaulating expressions */ + uint16_t n_watchpoints = gb->n_watchpoints; + gb->n_watchpoints = 0; + + value_t ret = ERROR; + + *error = false; + // Strip whitespace + while (length && (string[0] == ' ' || string[0] == '\n' || string[0] == '\r' || string[0] == '\t')) { + string++; + length--; + } + while (length && (string[length-1] == ' ' || string[length-1] == '\n' || string[length-1] == '\r' || string[length-1] == '\t')) { + length--; + } + if (length == 0) { + GB_log(gb, "Expected expression.\n"); + *error = true; + goto exit; + } + if (string[0] == '(' && string[length - 1] == ')') { + // Attempt to strip parentheses + signed depth = 0; + for (unsigned i = 0; i < length; i++) { + if (string[i] == '(') depth++; + if (depth == 0) { + // First and last are not matching + depth = 1; + break; + } + if (string[i] == ')') depth--; + } + if (depth == 0) { + ret = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + goto exit; + } + } + else if (string[0] == '[' && string[length - 1] == ']') { + // Attempt to strip square parentheses (memory dereference) + signed depth = 0; + for (unsigned i = 0; i < length; i++) { + if (string[i] == '[') depth++; + if (depth == 0) { + // First and last are not matching + depth = 1; + break; + } + if (string[i] == ']') depth--; + } + + if (depth == 0) { + value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + banking_state_t state; + if (addr.bank) { + save_banking_state(gb, &state); + switch_banking_state(gb, addr.bank); + } + ret = VALUE_16(GB_read_memory(gb, addr.value)); + if (addr.bank) { + restore_banking_state(gb, &state); + } + goto exit; + } + } + else if (string[0] == '{' && string[length - 1] == '}') { + // Attempt to strip curly parentheses (memory dereference) + signed depth = 0; + for (unsigned i = 0; i < length; i++) { + if (string[i] == '{') depth++; + if (depth == 0) { + // First and last are not matching + depth = 1; + break; + } + if (string[i] == '}') depth--; + } + + if (depth == 0) { + value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + banking_state_t state; + if (addr.bank) { + save_banking_state(gb, &state); + switch_banking_state(gb, addr.bank); + } + ret = VALUE_16(GB_read_memory(gb, addr.value) | (GB_read_memory(gb, addr.value + 1) * 0x100)); + if (addr.bank) { + restore_banking_state(gb, &state); + } + goto exit; + } + } + // Search for lowest priority operator + signed depth = 0; + unsigned operator_index = -1; + unsigned operator_pos = 0; + for (unsigned i = 0; i < length; i++) { + if (string[i] == '(') depth++; + else if (string[i] == ')') depth--; + else if (string[i] == '[') depth++; + else if (string[i] == ']') depth--; + else if (depth == 0) { + for (unsigned j = 0; j < sizeof(operators) / sizeof(operators[0]); j++) { + unsigned operator_length = strlen(operators[j].string); + if (operator_length > length - i) continue; // Operator too long + + if (memcmp(string + i, operators[j].string, operator_length) == 0) { + if (operator_index != -1 && operators[operator_index].priority < operators[j].priority) { + /* for supporting = vs ==, etc*/ + i += operator_length - 1; + break; + } + // Found an operator! + operator_pos = i; + operator_index = j; + /* for supporting = vs ==, etc*/ + i += operator_length - 1; + break; + } + } + } + } + if (operator_index != -1) { + unsigned right_start = (unsigned)(operator_pos + strlen(operators[operator_index].string)); + value_t right = debugger_evaluate(gb, string + right_start, length - right_start, error, watchpoint_address, watchpoint_new_value); + if (*error) goto exit; + if (operators[operator_index].lvalue_operator) { + lvalue_t left = debugger_evaluate_lvalue(gb, string, operator_pos, error, watchpoint_address, watchpoint_new_value); + if (*error) goto exit; + ret = operators[operator_index].lvalue_operator(gb, left, right.value); + goto exit; + } + value_t left = debugger_evaluate(gb, string, operator_pos, error, watchpoint_address, watchpoint_new_value); + if (*error) goto exit; + ret = operators[operator_index].operator(left, right); + goto exit; + } + + // Not an expression - must be a register or a literal + + // Registers + if (string[0] != '$' && (string[0] < '0' || string[0] > '9')) { + if (length == 1) { + switch (string[0]) { + case 'a': ret = VALUE_16(gb->registers[GB_REGISTER_AF] >> 8); goto exit; + case 'f': ret = VALUE_16(gb->registers[GB_REGISTER_AF] & 0xFF); goto exit; + case 'b': ret = VALUE_16(gb->registers[GB_REGISTER_BC] >> 8); goto exit; + case 'c': ret = VALUE_16(gb->registers[GB_REGISTER_BC] & 0xFF); goto exit; + case 'd': ret = VALUE_16(gb->registers[GB_REGISTER_DE] >> 8); goto exit; + case 'e': ret = VALUE_16(gb->registers[GB_REGISTER_DE] & 0xFF); goto exit; + case 'h': ret = VALUE_16(gb->registers[GB_REGISTER_HL] >> 8); goto exit; + case 'l': ret = VALUE_16(gb->registers[GB_REGISTER_HL] & 0xFF); goto exit; + } + } + else if (length == 2) { + switch (string[0]) { + case 'a': if (string[1] == 'f') {ret = VALUE_16(gb->registers[GB_REGISTER_AF]); goto exit;} + case 'b': if (string[1] == 'c') {ret = VALUE_16(gb->registers[GB_REGISTER_BC]); goto exit;} + case 'd': if (string[1] == 'e') {ret = VALUE_16(gb->registers[GB_REGISTER_DE]); goto exit;} + case 'h': if (string[1] == 'l') {ret = VALUE_16(gb->registers[GB_REGISTER_HL]); goto exit;} + case 's': if (string[1] == 'p') {ret = VALUE_16(gb->registers[GB_REGISTER_SP]); goto exit;} + case 'p': if (string[1] == 'c') {ret = (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}; goto exit;} + } + } + else if (length == 3) { + if (watchpoint_address && memcmp(string, "old", 3) == 0) { + ret = VALUE_16(GB_read_memory(gb, *watchpoint_address)); + goto exit; + } + + if (watchpoint_new_value && memcmp(string, "new", 3) == 0) { + ret = VALUE_16(*watchpoint_new_value); + goto exit; + } + + /* $new is identical to $old in read conditions */ + if (watchpoint_address && memcmp(string, "new", 3) == 0) { + ret = VALUE_16(GB_read_memory(gb, *watchpoint_address)); + goto exit; + } + } + + char symbol_name[length + 1]; + memcpy(symbol_name, string, length); + symbol_name[length] = 0; + const GB_symbol_t *symbol = GB_reversed_map_find_symbol(&gb->reversed_symbol_map, symbol_name); + if (symbol) { + ret = (value_t){true, symbol->bank, symbol->addr}; + goto exit; + } + + GB_log(gb, "Unknown register or symbol: %.*s\n", (unsigned) length, string); + *error = true; + goto exit; + } + + char *end; + unsigned base = 10; + if (string[0] == '$') { + string++; + base = 16; + length--; + } + uint16_t literal = (uint16_t) (strtol(string, &end, base)); + if (end != string + length) { + GB_log(gb, "Failed to parse: %.*s\n", (unsigned) length, string); + *error = true; + goto exit; + } + ret = VALUE_16(literal); +exit: + gb->n_watchpoints = n_watchpoints; + return ret; +} + +struct debugger_command_s; +typedef bool debugger_command_imp_t(GB_gameboy_t *gb, char *arguments, char *modifiers, const struct debugger_command_s *command); +typedef char *debugger_completer_imp_t(GB_gameboy_t *gb, const char *string, uintptr_t *context); + +typedef struct debugger_command_s { + const char *command; + uint8_t min_length; + debugger_command_imp_t *implementation; + const char *help_string; // Null if should not appear in help + const char *arguments_format; // For usage message + const char *modifiers_format; // For usage message + debugger_completer_imp_t *argument_completer; + debugger_completer_imp_t *modifiers_completer; +} debugger_command_t; + +static const char *lstrip(const char *str) +{ + while (*str == ' ' || *str == '\t') { + str++; + } + return str; +} + +#define STOPPED_ONLY \ +if (!gb->debug_stopped) { \ +GB_log(gb, "Program is running. \n"); \ +return false; \ +} + +#define NO_MODIFIERS \ +if (modifiers) { \ +print_usage(gb, command); \ +return true; \ +} + +static void print_usage(GB_gameboy_t *gb, const debugger_command_t *command) +{ + GB_log(gb, "Usage: %s", command->command); + + if (command->modifiers_format) { + GB_log(gb, "[/%s]", command->modifiers_format); + } + + if (command->arguments_format) { + GB_log(gb, " %s", command->arguments_format); + } + + GB_log(gb, "\n"); +} + +static bool cont(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + STOPPED_ONLY + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + gb->debug_stopped = false; + return false; +} + +static bool next(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + STOPPED_ONLY + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + gb->debug_stopped = false; + gb->debug_next_command = true; + gb->debug_call_depth = 0; + return false; +} + +static bool step(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + STOPPED_ONLY + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + return false; +} + +static bool finish(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + STOPPED_ONLY + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + gb->debug_stopped = false; + gb->debug_fin_command = true; + gb->debug_call_depth = 0; + return false; +} + +static bool stack_leak_detection(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + STOPPED_ONLY + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + gb->debug_stopped = false; + gb->stack_leak_detection = true; + gb->debug_call_depth = 0; + return false; +} + +static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + + GB_log(gb, "AF = $%04x (%c%c%c%c)\n", gb->registers[GB_REGISTER_AF], /* AF can't really be an address */ + (gb->f & GB_CARRY_FLAG)? 'C' : '-', + (gb->f & GB_HALF_CARRY_FLAG)? 'H' : '-', + (gb->f & GB_SUBTRACT_FLAG)? 'N' : '-', + (gb->f & GB_ZERO_FLAG)? 'Z' : '-'); + GB_log(gb, "BC = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_BC], false)); + GB_log(gb, "DE = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_DE], false)); + GB_log(gb, "HL = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_HL], false)); + GB_log(gb, "SP = %s\n", value_to_string(gb, gb->registers[GB_REGISTER_SP], false)); + GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false)); + GB_log(gb, "IME = %s\n", gb->ime? "Enabled" : "Disabled"); + return true; +} + +static char *on_off_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"on", "off"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +/* Enable or disable software breakpoints */ +static bool softbreak(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strcmp(lstrip(arguments), "on") == 0 || !strlen(lstrip(arguments))) { + gb->has_software_breakpoints = true; + } + else if (strcmp(lstrip(arguments), "off") == 0) { + gb->has_software_breakpoints = false; + } + else { + print_usage(gb, command); + } + + return true; +} + +/* Find the index of the closest breakpoint equal or greater to addr */ +static uint16_t find_breakpoint(GB_gameboy_t *gb, value_t addr) +{ + if (!gb->breakpoints) { + return 0; + } + + uint32_t key = BP_KEY(addr); + + unsigned min = 0; + unsigned max = gb->n_breakpoints; + while (min < max) { + uint16_t pivot = (min + max) / 2; + if (gb->breakpoints[pivot].key == key) return pivot; + if (gb->breakpoints[pivot].key > key) { + max = pivot; + } + else { + min = pivot + 1; + } + } + return (uint16_t) min; +} + +static inline bool is_legal_symbol_char(char c) +{ + if (c >= '0' && c <= '9') return true; + if (c >= 'A' && c <= 'Z') return true; + if (c >= 'a' && c <= 'z') return true; + if (c == '_') return true; + if (c == '.') return true; + return false; +} + +static char *symbol_completer(GB_gameboy_t *gb, const char *string, uintptr_t *_context) +{ + const char *symbol_prefix = string; + while (*string) { + if (!is_legal_symbol_char(*string)) { + symbol_prefix = string + 1; + } + string++; + } + + if (*symbol_prefix == '$') { + return NULL; + } + + struct { + uint16_t bank; + uint32_t symbol; + } *context = (void *)_context; + + + size_t length = strlen(symbol_prefix); + while (context->bank < 0x200) { + if (gb->bank_symbols[context->bank] == NULL || + context->symbol >= gb->bank_symbols[context->bank]->n_symbols) { + context->bank++; + context->symbol = 0; + continue; + } + const char *candidate = gb->bank_symbols[context->bank]->symbols[context->symbol++].name; + if (memcmp(symbol_prefix, candidate, length) == 0) { + return strdup(candidate + length); + } + } + return NULL; +} + +static char *j_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"j"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + bool is_jump_to = true; + if (!modifiers) { + is_jump_to = false; + } + else if (strcmp(modifiers, "j") != 0) { + print_usage(gb, command); + return true; + } + + if (strlen(lstrip(arguments)) == 0) { + print_usage(gb, command); + return true; + } + + if (gb->n_breakpoints == (typeof(gb->n_breakpoints)) -1) { + GB_log(gb, "Too many breakpoints set\n"); + return true; + } + + char *condition = NULL; + if ((condition = strstr(arguments, " if "))) { + *condition = 0; + condition += strlen(" if "); + /* Verify condition is sane (Todo: This might have side effects!) */ + bool error; + debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, NULL, NULL); + if (error) return true; + + } + + bool error; + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + uint32_t key = BP_KEY(result); + + if (error) return true; + + uint16_t index = find_breakpoint(gb, result); + if (index < gb->n_breakpoints && gb->breakpoints[index].key == key) { + GB_log(gb, "Breakpoint already set at %s\n", debugger_value_to_string(gb, result, true)); + if (!gb->breakpoints[index].condition && condition) { + GB_log(gb, "Added condition to breakpoint\n"); + gb->breakpoints[index].condition = strdup(condition); + } + else if (gb->breakpoints[index].condition && condition) { + GB_log(gb, "Replaced breakpoint condition\n"); + free(gb->breakpoints[index].condition); + gb->breakpoints[index].condition = strdup(condition); + } + else if (gb->breakpoints[index].condition && !condition) { + GB_log(gb, "Removed breakpoint condition\n"); + free(gb->breakpoints[index].condition); + gb->breakpoints[index].condition = NULL; + } + return true; + } + + gb->breakpoints = realloc(gb->breakpoints, (gb->n_breakpoints + 1) * sizeof(gb->breakpoints[0])); + memmove(&gb->breakpoints[index + 1], &gb->breakpoints[index], (gb->n_breakpoints - index) * sizeof(gb->breakpoints[0])); + gb->breakpoints[index].key = key; + + if (condition) { + gb->breakpoints[index].condition = strdup(condition); + } + else { + gb->breakpoints[index].condition = NULL; + } + gb->n_breakpoints++; + + gb->breakpoints[index].is_jump_to = is_jump_to; + + if (is_jump_to) { + gb->has_jump_to_breakpoints = true; + } + + GB_log(gb, "Breakpoint set at %s\n", debugger_value_to_string(gb, result, true)); + return true; +} + +static bool delete(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strlen(lstrip(arguments)) == 0) { + for (unsigned i = gb->n_breakpoints; i--;) { + if (gb->breakpoints[i].condition) { + free(gb->breakpoints[i].condition); + } + } + free(gb->breakpoints); + gb->breakpoints = NULL; + gb->n_breakpoints = 0; + return true; + } + + bool error; + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + uint32_t key = BP_KEY(result); + + if (error) return true; + + uint16_t index = 0; + for (unsigned i = 0; i < gb->n_breakpoints; i++) { + if (gb->breakpoints[i].key == key) { + /* Full match */ + index = i; + break; + } + if (gb->breakpoints[i].addr == result.value && result.has_bank != (gb->breakpoints[i].bank != (uint16_t) -1)) { + /* Partial match */ + index = i; + } + } + + if (index >= gb->n_breakpoints) { + GB_log(gb, "No breakpoint set at %s\n", debugger_value_to_string(gb, result, true)); + return true; + } + + result.bank = gb->breakpoints[index].bank; + result.has_bank = gb->breakpoints[index].bank != (uint16_t) -1; + + if (gb->breakpoints[index].condition) { + free(gb->breakpoints[index].condition); + } + + if (gb->breakpoints[index].is_jump_to) { + gb->has_jump_to_breakpoints = false; + for (unsigned i = 0; i < gb->n_breakpoints; i++) { + if (i == index) continue; + if (gb->breakpoints[i].is_jump_to) { + gb->has_jump_to_breakpoints = true; + break; + } + } + } + + memmove(&gb->breakpoints[index], &gb->breakpoints[index + 1], (gb->n_breakpoints - index - 1) * sizeof(gb->breakpoints[0])); + gb->n_breakpoints--; + gb->breakpoints = realloc(gb->breakpoints, gb->n_breakpoints * sizeof(gb->breakpoints[0])); + + GB_log(gb, "Breakpoint removed from %s\n", debugger_value_to_string(gb, result, true)); + return true; +} + +/* Find the index of the closest watchpoint equal or greater to addr */ +static uint16_t find_watchpoint(GB_gameboy_t *gb, value_t addr) +{ + if (!gb->watchpoints) { + return 0; + } + uint32_t key = WP_KEY(addr); + unsigned min = 0; + unsigned max = gb->n_watchpoints; + while (min < max) { + uint16_t pivot = (min + max) / 2; + if (gb->watchpoints[pivot].key == key) return pivot; + if (gb->watchpoints[pivot].key > key) { + max = pivot; + } + else { + min = pivot + 1; + } + } + return (uint16_t) min; +} + +static char *rw_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"r", "rw", "w"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +static bool watch(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + if (strlen(lstrip(arguments)) == 0) { +print_usage: + print_usage(gb, command); + return true; + } + + if (gb->n_watchpoints == (typeof(gb->n_watchpoints)) -1) { + GB_log(gb, "Too many watchpoints set\n"); + return true; + } + + if (!modifiers) { + modifiers = "w"; + } + + uint8_t flags = 0; + while (*modifiers) { + switch (*modifiers) { + case 'r': + flags |= GB_WATCHPOINT_R; + break; + case 'w': + flags |= GB_WATCHPOINT_W; + break; + default: + goto print_usage; + } + modifiers++; + } + + if (!flags) { + goto print_usage; + } + + char *condition = NULL; + if ((condition = strstr(arguments, " if "))) { + *condition = 0; + condition += strlen(" if "); + /* Verify condition is sane (Todo: This might have side effects!) */ + bool error; + /* To make $new and $old legal */ + uint16_t dummy = 0; + uint8_t dummy2 = 0; + debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, &dummy, &dummy2); + if (error) return true; + + } + + bool error; + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + uint32_t key = WP_KEY(result); + + if (error) return true; + + uint16_t index = find_watchpoint(gb, result); + if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) { + GB_log(gb, "Watchpoint already set at %s\n", debugger_value_to_string(gb, result, true)); + if (gb->watchpoints[index].flags != flags) { + GB_log(gb, "Modified watchpoint type\n"); + gb->watchpoints[index].flags = flags; + } + if (!gb->watchpoints[index].condition && condition) { + GB_log(gb, "Added condition to watchpoint\n"); + gb->watchpoints[index].condition = strdup(condition); + } + else if (gb->watchpoints[index].condition && condition) { + GB_log(gb, "Replaced watchpoint condition\n"); + free(gb->watchpoints[index].condition); + gb->watchpoints[index].condition = strdup(condition); + } + else if (gb->watchpoints[index].condition && !condition) { + GB_log(gb, "Removed watchpoint condition\n"); + free(gb->watchpoints[index].condition); + gb->watchpoints[index].condition = NULL; + } + return true; + } + + gb->watchpoints = realloc(gb->watchpoints, (gb->n_watchpoints + 1) * sizeof(gb->watchpoints[0])); + memmove(&gb->watchpoints[index + 1], &gb->watchpoints[index], (gb->n_watchpoints - index) * sizeof(gb->watchpoints[0])); + gb->watchpoints[index].key = key; + gb->watchpoints[index].flags = flags; + if (condition) { + gb->watchpoints[index].condition = strdup(condition); + } + else { + gb->watchpoints[index].condition = NULL; + } + gb->n_watchpoints++; + + GB_log(gb, "Watchpoint set at %s\n", debugger_value_to_string(gb, result, true)); + return true; +} + +static bool unwatch(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strlen(lstrip(arguments)) == 0) { + for (unsigned i = gb->n_watchpoints; i--;) { + if (gb->watchpoints[i].condition) { + free(gb->watchpoints[i].condition); + } + } + free(gb->watchpoints); + gb->watchpoints = NULL; + gb->n_watchpoints = 0; + return true; + } + + bool error; + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + uint32_t key = WP_KEY(result); + + if (error) return true; + + uint16_t index = 0; + for (unsigned i = 0; i < gb->n_watchpoints; i++) { + if (gb->watchpoints[i].key == key) { + /* Full match */ + index = i; + break; + } + if (gb->watchpoints[i].addr == result.value && result.has_bank != (gb->watchpoints[i].bank != (uint16_t) -1)) { + /* Partial match */ + index = i; + } + } + + if (index >= gb->n_watchpoints) { + GB_log(gb, "No watchpoint set at %s\n", debugger_value_to_string(gb, result, true)); + return true; + } + + result.bank = gb->watchpoints[index].bank; + result.has_bank = gb->watchpoints[index].bank != (uint16_t) -1; + + if (gb->watchpoints[index].condition) { + free(gb->watchpoints[index].condition); + } + + memmove(&gb->watchpoints[index], &gb->watchpoints[index + 1], (gb->n_watchpoints - index - 1) * sizeof(gb->watchpoints[0])); + gb->n_watchpoints--; + gb->watchpoints = realloc(gb->watchpoints, gb->n_watchpoints *sizeof(gb->watchpoints[0])); + + GB_log(gb, "Watchpoint removed from %s\n", debugger_value_to_string(gb, result, true)); + return true; +} + +static bool list(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + if (gb->n_breakpoints == 0) { + GB_log(gb, "No breakpoints set.\n"); + } + else { + GB_log(gb, "%d breakpoint(s) set:\n", gb->n_breakpoints); + for (uint16_t i = 0; i < gb->n_breakpoints; i++) { + value_t addr = (value_t){gb->breakpoints[i].bank != (uint16_t)-1, gb->breakpoints[i].bank, gb->breakpoints[i].addr}; + if (gb->breakpoints[i].condition) { + GB_log(gb, " %d. %s (%sCondition: %s)\n", i + 1, + debugger_value_to_string(gb, addr, addr.has_bank), + gb->breakpoints[i].is_jump_to? "Jump to, ": "", + gb->breakpoints[i].condition); + } + else { + GB_log(gb, " %d. %s%s\n", i + 1, + debugger_value_to_string(gb, addr, addr.has_bank), + gb->breakpoints[i].is_jump_to? " (Jump to)" : ""); + } + } + } + + if (gb->n_watchpoints == 0) { + GB_log(gb, "No watchpoints set.\n"); + } + else { + GB_log(gb, "%d watchpoint(s) set:\n", gb->n_watchpoints); + for (uint16_t i = 0; i < gb->n_watchpoints; i++) { + value_t addr = (value_t){gb->watchpoints[i].bank != (uint16_t)-1, gb->watchpoints[i].bank, gb->watchpoints[i].addr}; + if (gb->watchpoints[i].condition) { + GB_log(gb, " %d. %s (%c%c, Condition: %s)\n", i + 1, debugger_value_to_string(gb, addr, addr.has_bank), + (gb->watchpoints[i].flags & GB_WATCHPOINT_R)? 'r' : '-', + (gb->watchpoints[i].flags & GB_WATCHPOINT_W)? 'w' : '-', + gb->watchpoints[i].condition); + } + else { + GB_log(gb, " %d. %s (%c%c)\n", i + 1, debugger_value_to_string(gb, addr, addr.has_bank), + (gb->watchpoints[i].flags & GB_WATCHPOINT_R)? 'r' : '-', + (gb->watchpoints[i].flags & GB_WATCHPOINT_W)? 'w' : '-'); + } + } + } + + return true; +} + +static bool _should_break(GB_gameboy_t *gb, value_t addr, bool jump_to) +{ + uint16_t index = find_breakpoint(gb, addr); + uint32_t key = BP_KEY(addr); + + if (index < gb->n_breakpoints && gb->breakpoints[index].key == key && gb->breakpoints[index].is_jump_to == jump_to) { + if (!gb->breakpoints[index].condition) { + return true; + } + bool error; + bool condition = debugger_evaluate(gb, gb->breakpoints[index].condition, + (unsigned)strlen(gb->breakpoints[index].condition), &error, NULL, NULL).value; + if (error) { + /* Should never happen */ + GB_log(gb, "An internal error has occured\n"); + return true; + } + return condition; + } + return false; +} + +static bool should_break(GB_gameboy_t *gb, uint16_t addr, bool jump_to) +{ + /* Try any-bank breakpoint */ + value_t full_addr = (VALUE_16(addr)); + if (_should_break(gb, full_addr, jump_to)) return true; + + /* Try bank-specific breakpoint */ + full_addr.has_bank = true; + full_addr.bank = bank_for_addr(gb, addr); + return _should_break(gb, full_addr, jump_to); +} + +static char *format_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"a", "b", "d", "o", "x"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + if (strlen(lstrip(arguments)) == 0) { + print_usage(gb, command); + return true; + } + + if (!modifiers || !modifiers[0]) { + modifiers = "a"; + } + else if (modifiers[1]) { + print_usage(gb, command); + return true; + } + + bool error; + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + if (!error) { + switch (modifiers[0]) { + case 'a': + GB_log(gb, "=%s\n", debugger_value_to_string(gb, result, false)); + break; + case 'd': + GB_log(gb, "=%d\n", result.value); + break; + case 'x': + GB_log(gb, "=$%x\n", result.value); + break; + case 'o': + GB_log(gb, "=0%o\n", result.value); + break; + case 'b': + { + if (!result.value) { + GB_log(gb, "=%%0\n"); + break; + } + char binary[17]; + binary[16] = 0; + char *ptr = &binary[16]; + while (result.value) { + *(--ptr) = (result.value & 1)? '1' : '0'; + result.value >>= 1; + } + GB_log(gb, "=%%%s\n", ptr); + break; + } + default: + break; + } + } + return true; +} + +static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + if (strlen(lstrip(arguments)) == 0) { + print_usage(gb, command); + return true; + } + + bool error; + value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + uint16_t count = 32; + + if (modifiers) { + char *end; + count = (uint16_t) (strtol(modifiers, &end, 10)); + if (*end) { + print_usage(gb, command); + return true; + } + } + + if (!error) { + if (addr.has_bank) { + banking_state_t old_state; + save_banking_state(gb, &old_state); + switch_banking_state(gb, addr.bank); + + while (count) { + GB_log(gb, "%02x:%04x: ", addr.bank, addr.value); + for (unsigned i = 0; i < 16 && count; i++) { + GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); + count--; + } + addr.value += 16; + GB_log(gb, "\n"); + } + + restore_banking_state(gb, &old_state); + } + else { + while (count) { + GB_log(gb, "%04x: ", addr.value); + for (unsigned i = 0; i < 16 && count; i++) { + GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); + count--; + } + addr.value += 16; + GB_log(gb, "\n"); + } + } + } + return true; +} + +static bool disassemble(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + if (strlen(lstrip(arguments)) == 0) { + arguments = "pc"; + } + + bool error; + value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + uint16_t count = 5; + + if (modifiers) { + char *end; + count = (uint16_t) (strtol(modifiers, &end, 10)); + if (*end) { + print_usage(gb, command); + return true; + } + } + + if (!error) { + if (addr.has_bank) { + banking_state_t old_state; + save_banking_state(gb, &old_state); + switch_banking_state(gb, addr.bank); + + GB_cpu_disassemble(gb, addr.value, count); + + restore_banking_state(gb, &old_state); + } + else { + GB_cpu_disassemble(gb, addr.value, count); + } + } + return true; +} + +static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + const GB_cartridge_t *cartridge = gb->cartridge_type; + + if (cartridge->has_ram) { + GB_log(gb, "Cartridge includes%s RAM: $%x bytes\n", cartridge->has_battery? " battery-backed": "", gb->mbc_ram_size); + } + else { + GB_log(gb, "No cartridge RAM\n"); + } + + if (cartridge->mbc_type) { + if (gb->is_mbc30) { + GB_log(gb, "MBC30\n"); + } + else { + static const char *const mapper_names[] = { + [GB_MBC1] = "MBC1", + [GB_MBC2] = "MBC2", + [GB_MBC3] = "MBC3", + [GB_MBC5] = "MBC5", + [GB_HUC1] = "HUC-1", + [GB_HUC3] = "HUC-3", + }; + GB_log(gb, "%s\n", mapper_names[cartridge->mbc_type]); + } + GB_log(gb, "Current mapped ROM bank: %x\n", gb->mbc_rom_bank); + if (cartridge->has_ram) { + GB_log(gb, "Current mapped RAM bank: %x\n", gb->mbc_ram_bank); + if (gb->cartridge_type->mbc_type != GB_HUC1) { + GB_log(gb, "RAM is curently %s\n", gb->mbc_ram_enable? "enabled" : "disabled"); + } + } + if (cartridge->mbc_type == GB_MBC1 && gb->mbc1_wiring == GB_STANDARD_MBC1_WIRING) { + GB_log(gb, "MBC1 banking mode is %s\n", gb->mbc1.mode == 1 ? "RAM" : "ROM"); + } + if (cartridge->mbc_type == GB_MBC1 && gb->mbc1_wiring == GB_MBC1M_WIRING) { + GB_log(gb, "MBC1 uses MBC1M wiring. \n"); + GB_log(gb, "Current mapped ROM0 bank: %x\n", gb->mbc_rom0_bank); + GB_log(gb, "MBC1 multicart banking mode is %s\n", gb->mbc1.mode == 1 ? "enabled" : "disabled"); + } + + } + else { + GB_log(gb, "No MBC\n"); + } + + if (cartridge->has_rumble) { + GB_log(gb, "Cart contains a Rumble Pak\n"); + } + + if (cartridge->has_rtc) { + GB_log(gb, "Cart contains a real time clock\n"); + } + + return true; +} + +static bool backtrace(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + GB_log(gb, " 1. %s\n", debugger_value_to_string(gb, (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}, true)); + for (unsigned i = gb->backtrace_size; i--;) { + GB_log(gb, "%3d. %s\n", gb->backtrace_size - i + 1, debugger_value_to_string(gb, (value_t){true, gb->backtrace_returns[i].bank, gb->backtrace_returns[i].addr}, true)); + } + + return true; +} + +static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + STOPPED_ONLY + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + GB_log(gb, "Ticks: %llu. (Resetting)\n", (unsigned long long)gb->debugger_ticks); + gb->debugger_ticks = 0; + + return true; +} + + +static bool palettes(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + if (!GB_is_cgb(gb)) { + GB_log(gb, "Not available on a DMG.\n"); + return true; + } + + GB_log(gb, "Background palettes: \n"); + for (unsigned i = 0; i < 32; i++) { + GB_log(gb, "%04x ", ((uint16_t *)&gb->background_palettes_data)[i]); + if (i % 4 == 3) { + GB_log(gb, "\n"); + } + } + + GB_log(gb, "Sprites palettes: \n"); + for (unsigned i = 0; i < 32; i++) { + GB_log(gb, "%04x ", ((uint16_t *)&gb->sprite_palettes_data)[i]); + if (i % 4 == 3) { + GB_log(gb, "\n"); + } + } + + return true; +} + +static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + GB_log(gb, "LCDC:\n"); + GB_log(gb, " LCD enabled: %s\n",(gb->io_registers[GB_IO_LCDC] & 128)? "Enabled" : "Disabled"); + GB_log(gb, " %s: %s\n", (gb->cgb_mode? "Sprite priority flags" : "Background and Window"), + (gb->io_registers[GB_IO_LCDC] & 1)? "Enabled" : "Disabled"); + GB_log(gb, " Objects: %s\n", (gb->io_registers[GB_IO_LCDC] & 2)? "Enabled" : "Disabled"); + GB_log(gb, " Object size: %s\n", (gb->io_registers[GB_IO_LCDC] & 4)? "8x16" : "8x8"); + GB_log(gb, " Background tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & 8)? "$9C00" : "$9800"); + GB_log(gb, " Background and Window Tileset: %s\n", (gb->io_registers[GB_IO_LCDC] & 16)? "$8000" : "$8800"); + GB_log(gb, " Window: %s\n", (gb->io_registers[GB_IO_LCDC] & 32)? "Enabled" : "Disabled"); + GB_log(gb, " Window tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & 64)? "$9C00" : "$9800"); + + GB_log(gb, "\nSTAT:\n"); + static const char *modes[] = {"Mode 0, H-Blank", "Mode 1, V-Blank", "Mode 2, OAM", "Mode 3, Rendering"}; + GB_log(gb, " Current mode: %s\n", modes[gb->io_registers[GB_IO_STAT] & 3]); + GB_log(gb, " LYC flag: %s\n", (gb->io_registers[GB_IO_STAT] & 4)? "On" : "Off"); + GB_log(gb, " H-Blank interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 8)? "Enabled" : "Disabled"); + GB_log(gb, " V-Blank interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 16)? "Enabled" : "Disabled"); + GB_log(gb, " OAM interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 32)? "Enabled" : "Disabled"); + GB_log(gb, " LYC interrupt: %s\n", (gb->io_registers[GB_IO_STAT] & 64)? "Enabled" : "Disabled"); + + + + GB_log(gb, "\nCurrent line: %d\n", gb->current_line); + GB_log(gb, "Current state: "); + if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { + GB_log(gb, "Off\n"); + } + else if (gb->display_state == 7 || gb->display_state == 8) { + GB_log(gb, "Reading OAM data (%d/40)\n", gb->display_state == 8? gb->oam_search_index : 0); + } + else if (gb->display_state <= 3 || gb->display_state == 24 || gb->display_state == 31) { + GB_log(gb, "Glitched line 0 OAM mode (%d cycles to next event)\n", -gb->display_cycles / 2); + } + else if (gb->mode_for_interrupt == 3) { + signed pixel = gb->position_in_line > 160? (int8_t) gb->position_in_line : gb->position_in_line; + GB_log(gb, "Rendering pixel (%d/160)\n", pixel); + } + else { + GB_log(gb, "Sleeping (%d cycles to next event)\n", -gb->display_cycles / 2); + } + GB_log(gb, "LY: %d\n", gb->io_registers[GB_IO_LY]); + GB_log(gb, "LYC: %d\n", gb->io_registers[GB_IO_LYC]); + GB_log(gb, "Window position: %d, %d\n", (signed) gb->io_registers[GB_IO_WX] - 7, gb->io_registers[GB_IO_WY]); + GB_log(gb, "Interrupt line: %s\n", gb->stat_interrupt_line? "On" : "Off"); + + return true; +} + +static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + + GB_log(gb, "Current state: "); + if (!gb->apu.global_enable) { + GB_log(gb, "Disabled\n"); + } + else { + GB_log(gb, "Enabled\n"); + for (uint8_t channel = 0; channel < GB_N_CHANNELS; channel++) { + GB_log(gb, "CH%u is %s, DAC %s; current sample = 0x%x\n", channel + 1, + gb->apu.is_active[channel] ? "active " : "inactive", + GB_apu_is_DAC_enabled(gb, channel) ? "active " : "inactive", + gb->apu.samples[channel]); + } + } + + GB_log(gb, "SO1 (left output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x07); + if (gb->io_registers[GB_IO_NR51] & 0x0f) { + for (uint8_t channel = 0, mask = 0x01; channel < GB_N_CHANNELS; channel++, mask <<= 1) { + if (gb->io_registers[GB_IO_NR51] & mask) { + GB_log(gb, " CH%u", channel + 1); + } + } + } + else { + GB_log(gb, " no channels"); + } + GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); + + GB_log(gb, "SO2 (right output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x70 >> 4); + if (gb->io_registers[GB_IO_NR51] & 0xf0) { + for (uint8_t channel = 0, mask = 0x10; channel < GB_N_CHANNELS; channel++, mask <<= 1) { + if (gb->io_registers[GB_IO_NR51] & mask) { + GB_log(gb, " CH%u", channel + 1); + } + } + } + else { + GB_log(gb, " no channels"); + } + GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); + + + for (uint8_t channel = GB_SQUARE_1; channel <= GB_SQUARE_2; channel++) { + GB_log(gb, "\nCH%u:\n", channel + 1); + GB_log(gb, " Current volume: %u, current sample length: %u APU ticks (next in %u ticks)\n", + gb->apu.square_channels[channel].current_volume, + (gb->apu.square_channels[channel].sample_length ^ 0x7FF) * 2 + 1, + gb->apu.square_channels[channel].sample_countdown); + + uint8_t nrx2 = gb->io_registers[channel == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; + GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n", + gb->apu.square_channels[channel].volume_countdown, + nrx2 & 8 ? "in" : "de", + nrx2 & 7); + + uint8_t duty = gb->io_registers[channel == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; + GB_log(gb, " Duty cycle %s%% (%s), current index %u/8%s\n", + duty > 3? "" : (const char *[]){"12.5", " 25", " 50", " 75"}[duty], + duty > 3? "" : (const char *[]){"_______-", "-______-", "-____---", "_------_"}[duty], + gb->apu.square_channels[channel].current_sample_index & 0x7f, + gb->apu.square_channels[channel].current_sample_index >> 7 ? " (suppressed)" : ""); + + if (channel == GB_SQUARE_1) { + GB_log(gb, " Frequency sweep %s and %s (next in %u APU ticks)\n", + gb->apu.sweep_enabled? "active" : "inactive", + gb->apu.sweep_decreasing? "decreasing" : "increasing", + gb->apu.square_sweep_calculate_countdown); + } + + if (gb->apu.square_channels[channel].length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.square_channels[channel].pulse_length); + } + } + + + GB_log(gb, "\nCH3:\n"); + GB_log(gb, " Wave:"); + for (uint8_t i = 0; i < 32; i++) { + GB_log(gb, "%s%X", i%4?"":" ", gb->apu.wave_channel.wave_form[i]); + } + GB_log(gb, "\n"); + GB_log(gb, " Current position: %u\n", gb->apu.wave_channel.current_sample_index); + + GB_log(gb, " Volume %s (right-shifted %u times)\n", + gb->apu.wave_channel.shift > 4? "" : (const char *[]){"100%", "50%", "25%", "", "muted"}[gb->apu.wave_channel.shift], + gb->apu.wave_channel.shift); + + GB_log(gb, " Current sample length: %u APU ticks (next in %u ticks)\n", + gb->apu.wave_channel.sample_length ^ 0x7ff, + gb->apu.wave_channel.sample_countdown); + + if (gb->apu.wave_channel.length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.wave_channel.pulse_length); + } + + + GB_log(gb, "\nCH4:\n"); + GB_log(gb, " Current volume: %u, current sample length: %u APU ticks (next in %u ticks)\n", + gb->apu.noise_channel.current_volume, + gb->apu.noise_channel.sample_length * 4 + 3, + gb->apu.noise_channel.sample_countdown); + + GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n", + gb->apu.noise_channel.volume_countdown, + gb->io_registers[GB_IO_NR42] & 8 ? "in" : "de", + gb->io_registers[GB_IO_NR42] & 7); + + GB_log(gb, " LFSR in %u-step mode, current value ", + gb->apu.noise_channel.narrow? 7 : 15); + for (uint16_t lfsr = gb->apu.noise_channel.lfsr, i = 15; i--; lfsr <<= 1) { + GB_log(gb, "%u%s", (lfsr >> 14) & 1, i%4 ? "" : " "); + } + + if (gb->apu.noise_channel.length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.noise_channel.pulse_length); + } + + + GB_log(gb, "\n\nReminder: APU ticks are @ 2 MiHz\n"); + + return true; +} + +static char *wave_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"c", "f", "l"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (memcmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +static bool wave(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + if (strlen(lstrip(arguments)) || (modifiers && !strchr("fcl", modifiers[0]))) { + print_usage(gb, command); + return true; + } + + uint8_t shift_amount = 1, mask; + if (modifiers) { + switch (modifiers[0]) { + case 'c': + shift_amount = 2; + break; + case 'l': + shift_amount = 8; + break; + } + } + mask = (0xf << (shift_amount - 1)) & 0xf; + + for (int8_t cur_val = 0xf & mask; cur_val >= 0; cur_val -= shift_amount) { + for (uint8_t i = 0; i < 32; i++) { + if ((gb->apu.wave_channel.wave_form[i] & mask) == cur_val) { + GB_log(gb, "%X", gb->apu.wave_channel.wave_form[i]); + } + else { + GB_log(gb, "%c", i%4 == 2 ? '-' : ' '); + } + } + GB_log(gb, "\n"); + } + + return true; +} + +static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command); + +#define HELP_NEWLINE "\n " + +/* Commands without implementations are aliases of the previous non-alias commands */ +static const debugger_command_t commands[] = { + {"continue", 1, cont, "Continue running until next stop"}, + {"next", 1, next, "Run the next instruction, skipping over function calls"}, + {"step", 1, step, "Run the next instruction, stepping into function calls"}, + {"finish", 1, finish, "Run until the current function returns"}, + {"backtrace", 2, backtrace, "Displays the current call stack"}, + {"bt", 2, }, /* Alias */ + {"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected"}, + {"ticks", 2, ticks, "Displays the number of CPU ticks since the last time 'ticks' was" HELP_NEWLINE + "used"}, + {"registers", 1, registers, "Print values of processor registers and other important registers"}, + {"cartridge", 2, mbc, "Displays information about the MBC and cartridge"}, + {"mbc", 3, }, /* Alias */ + {"apu", 3, apu, "Displays information about the current state of the audio chip"}, + {"wave", 3, wave, "Prints a visual representation of the wave RAM." HELP_NEWLINE + "Modifiers can be used for a (f)ull print (the default)," HELP_NEWLINE + "a more (c)ompact one, or a one-(l)iner", "", "(f|c|l)", .modifiers_completer = wave_completer}, + {"lcd", 3, lcd, "Displays information about the current state of the LCD controller"}, + {"palettes", 3, palettes, "Displays the current CGB palettes"}, + {"softbreak", 2, softbreak, "Enables or disables software breakpoints", "(on|off)", .argument_completer = on_off_completer}, + {"breakpoint", 1, breakpoint, "Add a new breakpoint at the specified address/expression" HELP_NEWLINE + "Can also modify the condition of existing breakpoints." HELP_NEWLINE + "If the j modifier is used, the breakpoint will occur just before" HELP_NEWLINE + "jumping to the target.", + "[ if ]", "j", + .argument_completer = symbol_completer, .modifiers_completer = j_completer}, + {"delete", 2, delete, "Delete a breakpoint by its address, or all breakpoints", "[]", .argument_completer = symbol_completer}, + {"watch", 1, watch, "Add a new watchpoint at the specified address/expression." HELP_NEWLINE + "Can also modify the condition and type of existing watchpoints." HELP_NEWLINE + "Default watchpoint type is write-only.", + "[ if ]", "(r|w|rw)", + .argument_completer = symbol_completer, .modifiers_completer = rw_completer + }, + {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[]", .argument_completer = symbol_completer}, + {"list", 1, list, "List all set breakpoints and watchpoints"}, + {"print", 1, print, "Evaluate and print an expression" HELP_NEWLINE + "Use modifier to format as an address (a, default) or as a number in" HELP_NEWLINE + "decimal (d), hexadecimal (x), octal (o) or binary (b).", + "", "format", .argument_completer = symbol_completer, .modifiers_completer = format_completer}, + {"eval", 2, }, /* Alias */ + {"examine", 2, examine, "Examine values at address", "", "count", .argument_completer = symbol_completer}, + {"x", 1, }, /* Alias */ + {"disassemble", 1, disassemble, "Disassemble instructions at address", "", "count", .argument_completer = symbol_completer}, + + + {"help", 1, help, "List available commands or show help for the specified command", "[]"}, + {NULL,}, /* Null terminator */ +}; + +static const debugger_command_t *find_command(const char *string) +{ + size_t length = strlen(string); + for (const debugger_command_t *command = commands; command->command; command++) { + if (command->min_length > length) continue; + if (memcmp(command->command, string, length) == 0) { /* Is a substring? */ + /* Aliases */ + while (!command->implementation) { + command--; + } + return command; + } + } + + return NULL; +} + +static void print_command_shortcut(GB_gameboy_t *gb, const debugger_command_t *command) +{ + GB_attributed_log(gb, GB_LOG_BOLD | GB_LOG_UNDERLINE, "%.*s", command->min_length, command->command); + GB_attributed_log(gb, GB_LOG_BOLD, "%s", command->command + command->min_length); +} + +static void print_command_description(GB_gameboy_t *gb, const debugger_command_t *command) +{ + print_command_shortcut(gb, command); + GB_log(gb, ": "); + GB_log(gb, (const char *)&" %s\n" + strlen(command->command), command->help_string); +} + +static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *ignored) +{ + const debugger_command_t *command = find_command(arguments); + if (command) { + print_command_description(gb, command); + GB_log(gb, "\n"); + print_usage(gb, command); + + command++; + if (command->command && !command->implementation) { /* Command has aliases*/ + GB_log(gb, "\nAliases: "); + do { + print_command_shortcut(gb, command); + GB_log(gb, " "); + command++; + } while (command->command && !command->implementation); + GB_log(gb, "\n"); + } + return true; + } + for (command = commands; command->command; command++) { + if (command->help_string) { + print_command_description(gb, command); + } + } + return true; +} + +void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr) +{ + /* Called just after the CPU calls a function/enters an interrupt/etc... */ + + if (gb->stack_leak_detection) { + if (gb->debug_call_depth >= sizeof(gb->sp_for_call_depth) / sizeof(gb->sp_for_call_depth[0])) { + GB_log(gb, "Potential stack overflow detected (Functions nest too much). \n"); + gb->debug_stopped = true; + } + else { + gb->sp_for_call_depth[gb->debug_call_depth] = gb->registers[GB_REGISTER_SP]; + gb->addr_for_call_depth[gb->debug_call_depth] = gb->pc; + } + } + + if (gb->backtrace_size < sizeof(gb->backtrace_sps) / sizeof(gb->backtrace_sps[0])) { + + while (gb->backtrace_size) { + if (gb->backtrace_sps[gb->backtrace_size - 1] < gb->registers[GB_REGISTER_SP]) { + gb->backtrace_size--; + } + else { + break; + } + } + + gb->backtrace_sps[gb->backtrace_size] = gb->registers[GB_REGISTER_SP]; + gb->backtrace_returns[gb->backtrace_size].bank = bank_for_addr(gb, call_addr); + gb->backtrace_returns[gb->backtrace_size].addr = call_addr; + gb->backtrace_size++; + } + + gb->debug_call_depth++; +} + +void GB_debugger_ret_hook(GB_gameboy_t *gb) +{ + /* Called just before the CPU runs ret/reti */ + + gb->debug_call_depth--; + + if (gb->stack_leak_detection) { + if (gb->debug_call_depth < 0) { + GB_log(gb, "Function finished without a stack leak.\n"); + gb->debug_stopped = true; + } + else { + if (gb->registers[GB_REGISTER_SP] != gb->sp_for_call_depth[gb->debug_call_depth]) { + GB_log(gb, "Stack leak detected for function %s!\n", value_to_string(gb, gb->addr_for_call_depth[gb->debug_call_depth], true)); + GB_log(gb, "SP is $%04x, should be $%04x.\n", gb->registers[GB_REGISTER_SP], + gb->sp_for_call_depth[gb->debug_call_depth]); + gb->debug_stopped = true; + } + } + } + + while (gb->backtrace_size) { + if (gb->backtrace_sps[gb->backtrace_size - 1] <= gb->registers[GB_REGISTER_SP]) { + gb->backtrace_size--; + } + else { + break; + } + } +} + +static bool _GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, value_t addr, uint8_t value) +{ + uint16_t index = find_watchpoint(gb, addr); + uint32_t key = WP_KEY(addr); + + if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) { + if (!(gb->watchpoints[index].flags & GB_WATCHPOINT_W)) { + return false; + } + if (!gb->watchpoints[index].condition) { + gb->debug_stopped = true; + GB_log(gb, "Watchpoint: [%s] = $%02x\n", debugger_value_to_string(gb, addr, true), value); + return true; + } + bool error; + bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition, + (unsigned)strlen(gb->watchpoints[index].condition), &error, &addr.value, &value).value; + if (error) { + /* Should never happen */ + GB_log(gb, "An internal error has occured\n"); + return false; + } + if (condition) { + gb->debug_stopped = true; + GB_log(gb, "Watchpoint: [%s] = $%02x\n", debugger_value_to_string(gb, addr, true), value); + return true; + } + } + return false; +} + +void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + if (gb->debug_stopped) return; + + /* Try any-bank breakpoint */ + value_t full_addr = (VALUE_16(addr)); + if (_GB_debugger_test_write_watchpoint(gb, full_addr, value)) return; + + /* Try bank-specific breakpoint */ + full_addr.has_bank = true; + full_addr.bank = bank_for_addr(gb, addr); + _GB_debugger_test_write_watchpoint(gb, full_addr, value); +} + +static bool _GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, value_t addr) +{ + uint16_t index = find_watchpoint(gb, addr); + uint32_t key = WP_KEY(addr); + + if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) { + if (!(gb->watchpoints[index].flags & GB_WATCHPOINT_R)) { + return false; + } + if (!gb->watchpoints[index].condition) { + gb->debug_stopped = true; + GB_log(gb, "Watchpoint: [%s]\n", debugger_value_to_string(gb, addr, true)); + return true; + } + bool error; + bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition, + (unsigned)strlen(gb->watchpoints[index].condition), &error, &addr.value, NULL).value; + if (error) { + /* Should never happen */ + GB_log(gb, "An internal error has occured\n"); + return false; + } + if (condition) { + gb->debug_stopped = true; + GB_log(gb, "Watchpoint: [%s]\n", debugger_value_to_string(gb, addr, true)); + return true; + } + } + return false; +} + +void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr) +{ + if (gb->debug_stopped) return; + + /* Try any-bank breakpoint */ + value_t full_addr = (VALUE_16(addr)); + if (_GB_debugger_test_read_watchpoint(gb, full_addr)) return; + + /* Try bank-specific breakpoint */ + full_addr.has_bank = true; + full_addr.bank = bank_for_addr(gb, addr); + _GB_debugger_test_read_watchpoint(gb, full_addr); +} + +/* Returns true if debugger waits for more commands */ +bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input) +{ + if (!input[0]) { + return true; + } + + char *command_string = input; + char *arguments = strchr(input, ' '); + if (arguments) { + /* Actually "split" the string. */ + arguments[0] = 0; + arguments++; + } + else { + arguments = ""; + } + + char *modifiers = strchr(command_string, '/'); + if (modifiers) { + /* Actually "split" the string. */ + modifiers[0] = 0; + modifiers++; + } + + const debugger_command_t *command = find_command(command_string); + if (command) { + return command->implementation(gb, arguments, modifiers, command); + } + else { + GB_log(gb, "%s: no such command.\n", command_string); + return true; + } +} + +/* Returns true if debugger waits for more commands */ +char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *context) +{ + char *command_string = input; + char *arguments = strchr(input, ' '); + if (arguments) { + /* Actually "split" the string. */ + arguments[0] = 0; + arguments++; + } + + char *modifiers = strchr(command_string, '/'); + if (modifiers) { + /* Actually "split" the string. */ + modifiers[0] = 0; + modifiers++; + } + + const debugger_command_t *command = find_command(command_string); + if (command && command->implementation == help && arguments) { + command_string = arguments; + arguments = NULL; + } + + /* No commands and no modifiers, complete the command */ + if (!arguments && !modifiers) { + size_t length = strlen(command_string); + if (*context >= sizeof(commands) / sizeof(commands[0])) { + return NULL; + } + for (const debugger_command_t *command = &commands[*context]; command->command; command++) { + (*context)++; + if (memcmp(command->command, command_string, length) == 0) { /* Is a substring? */ + return strdup(command->command + length); + } + } + return NULL; + } + + if (command) { + if (arguments) { + if (command->argument_completer) { + return command->argument_completer(gb, arguments, context); + } + return NULL; + } + + if (modifiers) { + if (command->modifiers_completer) { + return command->modifiers_completer(gb, modifiers, context); + } + return NULL; + } + } + return NULL; +} + +typedef enum { + JUMP_TO_NONE, + JUMP_TO_BREAK, + JUMP_TO_NONTRIVIAL, +} jump_to_return_t; + +static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *address); + +void GB_debugger_run(GB_gameboy_t *gb) +{ + if (gb->debug_disable) return; + + char *input = NULL; + if (gb->debug_next_command && gb->debug_call_depth <= 0 && !gb->halted) { + gb->debug_stopped = true; + } + if (gb->debug_fin_command && gb->debug_call_depth == -1) { + gb->debug_stopped = true; + } + if (gb->debug_stopped) { + GB_cpu_disassemble(gb, gb->pc, 5); + } +next_command: + if (input) { + free(input); + } + if (gb->breakpoints && !gb->debug_stopped && should_break(gb, gb->pc, false)) { + gb->debug_stopped = true; + GB_log(gb, "Breakpoint: PC = %s\n", value_to_string(gb, gb->pc, true)); + GB_cpu_disassemble(gb, gb->pc, 5); + } + + if (gb->breakpoints && !gb->debug_stopped) { + uint16_t address = 0; + jump_to_return_t jump_to_result = test_jump_to_breakpoints(gb, &address); + + bool should_delete_state = true; + if (gb->nontrivial_jump_state && should_break(gb, gb->pc, true)) { + if (gb->non_trivial_jump_breakpoint_occured) { + gb->non_trivial_jump_breakpoint_occured = false; + } + else { + gb->non_trivial_jump_breakpoint_occured = true; + GB_log(gb, "Jumping to breakpoint: PC = %s\n", value_to_string(gb, gb->pc, true)); + GB_cpu_disassemble(gb, gb->pc, 5); + GB_load_state_from_buffer(gb, gb->nontrivial_jump_state, -1); + gb->debug_stopped = true; + } + } + else if (jump_to_result == JUMP_TO_BREAK) { + gb->debug_stopped = true; + GB_log(gb, "Jumping to breakpoint: PC = %s\n", value_to_string(gb, address, true)); + GB_cpu_disassemble(gb, gb->pc, 5); + gb->non_trivial_jump_breakpoint_occured = false; + } + else if (jump_to_result == JUMP_TO_NONTRIVIAL) { + if (!gb->nontrivial_jump_state) { + gb->nontrivial_jump_state = malloc(GB_get_save_state_size(gb)); + } + GB_save_state_to_buffer(gb, gb->nontrivial_jump_state); + gb->non_trivial_jump_breakpoint_occured = false; + should_delete_state = false; + } + else { + gb->non_trivial_jump_breakpoint_occured = false; + } + + if (should_delete_state) { + if (gb->nontrivial_jump_state) { + free(gb->nontrivial_jump_state); + gb->nontrivial_jump_state = NULL; + } + } + } + + if (gb->debug_stopped && !gb->debug_disable) { + gb->debug_next_command = false; + gb->debug_fin_command = false; + gb->stack_leak_detection = false; + input = gb->input_callback(gb); + + if (input == NULL) { + /* Debugging is no currently available, continue running */ + gb->debug_stopped = false; + return; + } + + if (GB_debugger_execute_command(gb, input)) { + goto next_command; + } + + free(input); + } +} + +void GB_debugger_handle_async_commands(GB_gameboy_t *gb) +{ + char *input = NULL; + + while (gb->async_input_callback && (input = gb->async_input_callback(gb))) { + GB_debugger_execute_command(gb, input); + free(input); + } +} + +void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol) +{ + bank &= 0x1FF; + + if (!gb->bank_symbols[bank]) { + gb->bank_symbols[bank] = GB_map_alloc(); + } + GB_bank_symbol_t *allocated_symbol = GB_map_add_symbol(gb->bank_symbols[bank], address, symbol); + if (allocated_symbol) { + GB_reversed_map_add_symbol(&gb->reversed_symbol_map, bank, allocated_symbol); + } +} + +void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "r"); + if (!f) return; + + char *line = NULL; + size_t size = 0; + size_t length = 0; + while ((length = getline(&line, &size, f)) != -1) { + for (unsigned i = 0; i < length; i++) { + if (line[i] == ';' || line[i] == '\n' || line[i] == '\r') { + line[i] = 0; + length = i; + break; + } + } + if (length == 0) continue; + + unsigned bank, address; + char symbol[length]; + + if (sscanf(line, "%x:%x %s", &bank, &address, symbol) == 3) { + GB_debugger_add_symbol(gb, bank, address, symbol); + } + } + free(line); + fclose(f); +} + +void GB_debugger_clear_symbols(GB_gameboy_t *gb) +{ + for (unsigned i = sizeof(gb->bank_symbols) / sizeof(gb->bank_symbols[0]); i--;) { + if (gb->bank_symbols[i]) { + GB_map_free(gb->bank_symbols[i]); + gb->bank_symbols[i] = 0; + } + } + for (unsigned i = sizeof(gb->reversed_symbol_map.buckets) / sizeof(gb->reversed_symbol_map.buckets[0]); i--;) { + while (gb->reversed_symbol_map.buckets[i]) { + GB_symbol_t *next = gb->reversed_symbol_map.buckets[i]->next; + free(gb->reversed_symbol_map.buckets[i]); + gb->reversed_symbol_map.buckets[i] = next; + } + } +} + +const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr) +{ + uint16_t bank = bank_for_addr(gb, addr); + + const GB_bank_symbol_t *symbol = GB_map_find_symbol(gb->bank_symbols[bank], addr); + if (symbol) return symbol; + if (bank != 0) return GB_map_find_symbol(gb->bank_symbols[0], addr); /* Maybe the symbol incorrectly uses bank 0? */ + + return NULL; +} + +const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr) +{ + const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, addr); + if (symbol && symbol->addr == addr) return symbol->name; + return NULL; +} + +/* The public version of debugger_evaluate */ +bool GB_debugger_evaluate(GB_gameboy_t *gb, const char *string, uint16_t *result, uint16_t *result_bank) +{ + bool error = false; + value_t value = debugger_evaluate(gb, string, strlen(string), &error, NULL, NULL); + if (result) { + *result = value.value; + } + if (result_bank) { + *result_bank = value.has_bank? value.value : -1; + } + return error; +} + +void GB_debugger_break(GB_gameboy_t *gb) +{ + gb->debug_stopped = true; +} + +bool GB_debugger_is_stopped(GB_gameboy_t *gb) +{ + return gb->debug_stopped; +} + +void GB_debugger_set_disabled(GB_gameboy_t *gb, bool disabled) +{ + gb->debug_disable = disabled; +} + +/* Jump-to breakpoints */ + +static bool is_in_trivial_memory(uint16_t addr) +{ + /* ROM */ + if (addr < 0x8000) { + return true; + } + + /* HRAM */ + if (addr >= 0xFF80 && addr < 0xFFFF) { + return true; + } + + /* RAM */ + if (addr >= 0xC000 && addr < 0xE000) { + return true; + } + + return false; +} + +typedef uint16_t GB_opcode_address_getter_t(GB_gameboy_t *gb, uint8_t opcode); + +uint16_t trivial_1(GB_gameboy_t *gb, uint8_t opcode) +{ + return gb->pc + 1; +} + +uint16_t trivial_2(GB_gameboy_t *gb, uint8_t opcode) +{ + return gb->pc + 2; +} + +uint16_t trivial_3(GB_gameboy_t *gb, uint8_t opcode) +{ + return gb->pc + 3; +} + +static uint16_t jr_r8(GB_gameboy_t *gb, uint8_t opcode) +{ + return gb->pc + 2 + (int8_t)GB_read_memory(gb, gb->pc + 1); +} + +static bool condition_code(GB_gameboy_t *gb, uint8_t opcode) +{ + switch ((opcode >> 3) & 0x3) { + case 0: + return !(gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + case 1: + return (gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + case 2: + return !(gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + case 3: + return (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + } + + return false; +} + +static uint16_t jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode) +{ + if (!condition_code(gb, opcode)) { + return gb->pc + 2; + } + + return gb->pc + 2 + (int8_t)GB_read_memory(gb, gb->pc + 1); +} + +static uint16_t ret(GB_gameboy_t *gb, uint8_t opcode) +{ + return GB_read_memory(gb, gb->registers[GB_REGISTER_SP]) | + (GB_read_memory(gb, gb->registers[GB_REGISTER_SP] + 1) << 8); +} + + +static uint16_t ret_cc(GB_gameboy_t *gb, uint8_t opcode) +{ + if (condition_code(gb, opcode)) { + return ret(gb, opcode); + } + else { + return gb->pc + 1; + } +} + +static uint16_t jp_a16(GB_gameboy_t *gb, uint8_t opcode) +{ + return GB_read_memory(gb, gb->pc + 1) | + (GB_read_memory(gb, gb->pc + 2) << 8); +} + +static uint16_t jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode) +{ + if (condition_code(gb, opcode)) { + return jp_a16(gb, opcode); + } + else { + return gb->pc + 3; + } +} + +static uint16_t rst(GB_gameboy_t *gb, uint8_t opcode) +{ + return opcode ^ 0xC7; +} + +static uint16_t jp_hl(GB_gameboy_t *gb, uint8_t opcode) +{ + return gb->hl; +} + +static GB_opcode_address_getter_t *opcodes[256] = { + /* X0 X1 X2 X3 X4 X5 X6 X7 */ + /* X8 X9 Xa Xb Xc Xd Xe Xf */ + trivial_1, trivial_3, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, /* 0X */ + trivial_3, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, + trivial_2, trivial_3, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, /* 1X */ + jr_r8, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, + jr_cc_r8, trivial_3, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, /* 2X */ + jr_cc_r8, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, + jr_cc_r8, trivial_3, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, /* 3X */ + jr_cc_r8, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, /* 4X */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, /* 5X */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, /* 6X */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, NULL, trivial_1, /* 7X */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, /* 8X */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, /* 9X */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, /* aX */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, /* bX */ + trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, trivial_1, + ret_cc, trivial_1, jp_cc_a16, jp_a16, jp_cc_a16, trivial_1, trivial_2, rst, /* cX */ + ret_cc, ret, jp_cc_a16, trivial_2, jp_cc_a16, jp_a16, trivial_2, rst, + ret_cc, trivial_1, jp_cc_a16, NULL, jp_cc_a16, trivial_1, trivial_2, rst, /* dX */ + ret_cc, ret, jp_cc_a16, NULL, jp_cc_a16, NULL, trivial_2, rst, + trivial_2, trivial_1, trivial_1, NULL, NULL, trivial_1, trivial_2, rst, /* eX */ + trivial_2, jp_hl, trivial_3, NULL, NULL, NULL, trivial_2, rst, + trivial_2, trivial_1, trivial_1, trivial_1, NULL, trivial_1, trivial_2, rst, /* fX */ + trivial_2, trivial_1, trivial_3, trivial_1, NULL, NULL, trivial_2, rst, +}; + +static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *address) +{ + if (!gb->has_jump_to_breakpoints) return JUMP_TO_NONE; + + if (!is_in_trivial_memory(gb->pc) || !is_in_trivial_memory(gb->pc + 2) || + !is_in_trivial_memory(gb->registers[GB_REGISTER_SP]) || !is_in_trivial_memory(gb->registers[GB_REGISTER_SP] + 1)) { + return JUMP_TO_NONTRIVIAL; + } + + /* Interrupts */ + if (gb->ime) { + for (unsigned i = 0; i < 5; i++) { + if ((gb->interrupt_enable & (1 << i)) && (gb->io_registers[GB_IO_IF] & (1 << i))) { + if (should_break(gb, 0x40 + i * 8, true)) { + if (address) { + *address = 0x40 + i * 8; + } + return JUMP_TO_BREAK; + } + } + } + } + + uint16_t n_watchpoints = gb->n_watchpoints; + gb->n_watchpoints = 0; + + uint8_t opcode = GB_read_memory(gb, gb->pc); + + if (opcode == 0x76) { + gb->n_watchpoints = n_watchpoints; + if (gb->ime) { /* Already handled in above */ + return JUMP_TO_NONE; + } + + if (gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F) { + return JUMP_TO_NONTRIVIAL; /* HALT bug could occur */ + } + + return JUMP_TO_NONE; + } + + GB_opcode_address_getter_t *getter = opcodes[opcode]; + if (!getter) { + gb->n_watchpoints = n_watchpoints; + return JUMP_TO_NONE; + } + + uint16_t new_pc = getter(gb, opcode); + + gb->n_watchpoints = n_watchpoints; + + if (address) { + *address = new_pc; + } + + return should_break(gb, new_pc, true) ? JUMP_TO_BREAK : JUMP_TO_NONE; +} diff --git a/bsnes/gb/Core/debugger.h b/bsnes/gb/Core/debugger.h new file mode 100644 index 00000000..0678b30c --- /dev/null +++ b/bsnes/gb/Core/debugger.h @@ -0,0 +1,46 @@ +#ifndef debugger_h +#define debugger_h +#include +#include +#include "gb_struct_def.h" +#include "symbol_hash.h" + + +#ifdef GB_INTERNAL +#ifdef GB_DISABLE_DEBUGGER +#define GB_debugger_run(gb) (void)0 +#define GB_debugger_handle_async_commands(gb) (void)0 +#define GB_debugger_ret_hook(gb) (void)0 +#define GB_debugger_call_hook(gb, addr) (void)addr +#define GB_debugger_test_write_watchpoint(gb, addr, value) ((void)addr, (void)value) +#define GB_debugger_test_read_watchpoint(gb, addr) (void)addr +#define GB_debugger_add_symbol(gb, bank, address, symbol) ((void)bank, (void)address, (void)symbol) + +#else +void GB_debugger_run(GB_gameboy_t *gb); +void GB_debugger_handle_async_commands(GB_gameboy_t *gb); +void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr); +void GB_debugger_ret_hook(GB_gameboy_t *gb); +void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr); +const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr); +void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol); +#endif /* GB_DISABLE_DEBUGGER */ +#endif + +#ifdef GB_INTERNAL +bool /* Returns true if debugger waits for more commands. Not relevant for non-GB_INTERNAL */ +#else +void +#endif +GB_debugger_execute_command(GB_gameboy_t *gb, char *input); /* Destroys input. */ +char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *context); /* Destroys input, result requires free */ + +void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path); +const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr); +bool GB_debugger_evaluate(GB_gameboy_t *gb, const char *string, uint16_t *result, uint16_t *result_bank); /* result_bank is -1 if unused. */ +void GB_debugger_break(GB_gameboy_t *gb); +bool GB_debugger_is_stopped(GB_gameboy_t *gb); +void GB_debugger_set_disabled(GB_gameboy_t *gb, bool disabled); +void GB_debugger_clear_symbols(GB_gameboy_t *gb); +#endif /* debugger_h */ diff --git a/bsnes/gb/Core/display.c b/bsnes/gb/Core/display.c new file mode 100644 index 00000000..2eb8c424 --- /dev/null +++ b/bsnes/gb/Core/display.c @@ -0,0 +1,1487 @@ +#include +#include +#include +#include +#include "gb.h" + +/* FIFO functions */ + +static inline unsigned fifo_size(GB_fifo_t *fifo) +{ + return (fifo->write_end - fifo->read_end) & (GB_FIFO_LENGTH - 1); +} + +static void fifo_clear(GB_fifo_t *fifo) +{ + fifo->read_end = fifo->write_end = 0; +} + +static GB_fifo_item_t *fifo_pop(GB_fifo_t *fifo) +{ + GB_fifo_item_t *ret = &fifo->fifo[fifo->read_end]; + fifo->read_end++; + fifo->read_end &= (GB_FIFO_LENGTH - 1); + return ret; +} + +static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint8_t palette, bool bg_priority, bool flip_x) +{ + if (!flip_x) { + UNROLL + for (unsigned i = 8; i--;) { + fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { + (lower >> 7) | ((upper >> 7) << 1), + palette, + 0, + bg_priority, + }; + lower <<= 1; + upper <<= 1; + + fifo->write_end++; + fifo->write_end &= (GB_FIFO_LENGTH - 1); + } + } + else { + UNROLL + for (unsigned i = 8; i--;) { + fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { + (lower & 1) | ((upper & 1) << 1), + palette, + 0, + bg_priority, + }; + lower >>= 1; + upper >>= 1; + + fifo->write_end++; + fifo->write_end &= (GB_FIFO_LENGTH - 1); + } + } +} + +static void fifo_overlay_object_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint8_t palette, bool bg_priority, uint8_t priority, bool flip_x) +{ + while (fifo_size(fifo) < 8) { + fifo->fifo[fifo->write_end] = (GB_fifo_item_t) {0,}; + fifo->write_end++; + fifo->write_end &= (GB_FIFO_LENGTH - 1); + } + + uint8_t flip_xor = flip_x? 0: 0x7; + + UNROLL + for (unsigned i = 8; i--;) { + uint8_t pixel = (lower >> 7) | ((upper >> 7) << 1); + GB_fifo_item_t *target = &fifo->fifo[(fifo->read_end + (i ^ flip_xor)) & (GB_FIFO_LENGTH - 1)]; + if (pixel != 0 && (target->pixel == 0 || target->priority > priority)) { + target->pixel = pixel; + target->palette = palette; + target->bg_priority = bg_priority; + target->priority = priority; + } + lower <<= 1; + upper <<= 1; + } +} + + +/* + Each line is 456 cycles. Without scrolling, sprites or a window: + Mode 2 - 80 cycles / OAM Transfer + Mode 3 - 172 cycles / Rendering + Mode 0 - 204 cycles / HBlank + + Mode 1 is VBlank + */ + +#define MODE2_LENGTH (80) +#define LINE_LENGTH (456) +#define LINES (144) +#define WIDTH (160) +#define BORDERED_WIDTH 256 +#define BORDERED_HEIGHT 224 +#define FRAME_LENGTH (LCDC_PERIOD) +#define VIRTUAL_LINES (FRAME_LENGTH / LINE_LENGTH) // = 154 + +typedef struct __attribute__((packed)) { + uint8_t y; + uint8_t x; + uint8_t tile; + uint8_t flags; +} GB_object_t; + +static void display_vblank(GB_gameboy_t *gb) +{ + gb->vblank_just_occured = true; + + /* TODO: Slow in turbo mode! */ + if (GB_is_hle_sgb(gb)) { + GB_sgb_render(gb); + } + + if (gb->turbo) { + if (GB_timing_sync_turbo(gb)) { + return; + } + } + + bool is_ppu_stopped = !GB_is_cgb(gb) && gb->stopped && gb->io_registers[GB_IO_LCDC] & 0x80; + + if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || is_ppu_stopped) || gb->cgb_repeated_a_frame)) { + /* LCD is off, set screen to white or black (if LCD is on in stop mode) */ + if (!GB_is_sgb(gb)) { + uint32_t color = 0; + if (GB_is_cgb(gb)) { + color = GB_convert_rgb15(gb, 0x7FFF, false); + } + else { + color = is_ppu_stopped ? + gb->background_palettes_rgb[0] : + gb->background_palettes_rgb[4]; + } + if (gb->border_mode == GB_BORDER_ALWAYS) { + for (unsigned y = 0; y < LINES; y++) { + for (unsigned x = 0; x < WIDTH; x++) { + gb ->screen[x + y * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH] = color; + } + } + } + else { + for (unsigned i = 0; i < WIDTH * LINES; i++) { + gb ->screen[i] = color; + } + } + } + } + + if (gb->border_mode == GB_BORDER_ALWAYS && !GB_is_sgb(gb)) { + GB_borrow_sgb_border(gb); + uint32_t border_colors[16 * 4]; + + if (!gb->has_sgb_border && GB_is_cgb(gb) && gb->model != GB_MODEL_AGB) { + static uint16_t colors[] = { + 0x2095, 0x5129, 0x1EAF, 0x1EBA, 0x4648, + 0x30DA, 0x69AD, 0x2B57, 0x2B5D, 0x632C, + 0x1050, 0x3C84, 0x0E07, 0x0E18, 0x2964, + }; + unsigned index = gb->rom? gb->rom[0x14e] % 5 : 0; + gb->borrowed_border.palette[0] = colors[index]; + gb->borrowed_border.palette[10] = colors[5 + index]; + gb->borrowed_border.palette[14] = colors[10 + index]; + + } + + for (unsigned i = 0; i < 16 * 4; i++) { + border_colors[i] = GB_convert_rgb15(gb, gb->borrowed_border.palette[i], true); + } + + for (unsigned tile_y = 0; tile_y < 28; tile_y++) { + for (unsigned tile_x = 0; tile_x < 32; tile_x++) { + if (tile_x >= 6 && tile_x < 26 && tile_y >= 5 && tile_y < 23) { + continue; + } + uint16_t tile = gb->borrowed_border.map[tile_x + tile_y * 32]; + uint8_t flip_x = (tile & 0x4000)? 0x7 : 0; + uint8_t flip_y = (tile & 0x8000)? 0x7 : 0; + uint8_t palette = (tile >> 10) & 3; + for (unsigned y = 0; y < 8; y++) { + for (unsigned x = 0; x < 8; x++) { + uint8_t color = gb->borrowed_border.tiles[(tile & 0xFF) * 64 + (x ^ flip_x) + (y ^ flip_y) * 8] & 0xF; + uint32_t *output = gb->screen + tile_x * 8 + x + (tile_y * 8 + y) * 256; + if (color == 0) { + *output = border_colors[0]; + } + else { + *output = border_colors[color + palette * 16]; + } + } + } + } + } + } + GB_handle_rumble(gb); + + if (gb->vblank_callback) { + gb->vblank_callback(gb); + } + GB_timing_sync(gb); +} + +static inline uint8_t scale_channel(uint8_t x) +{ + return (x << 3) | (x >> 2); +} + +static inline uint8_t scale_channel_with_curve(uint8_t x) +{ + return (uint8_t[]){0,5,8,11,16,22,28,36,43,51,59,67,77,87,97,107,119,130,141,153,166,177,188,200,209,221,230,238,245,249,252,255}[x]; +} + +static inline uint8_t scale_channel_with_curve_agb(uint8_t x) +{ + return (uint8_t[]){0,2,5,10,15,20,26,32,38,45,52,60,68,76,84,92,101,110,119,128,138,148,158,168,178,189,199,210,221,232,244,255}[x]; +} + +static inline uint8_t scale_channel_with_curve_sgb(uint8_t x) +{ + return (uint8_t[]){0,2,5,9,15,20,27,34,42,50,58,67,76,85,94,104,114,123,133,143,153,163,173,182,192,202,211,220,229,238,247,255}[x]; +} + + +uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border) +{ + uint8_t r = (color) & 0x1F; + uint8_t g = (color >> 5) & 0x1F; + uint8_t b = (color >> 10) & 0x1F; + + if (gb->color_correction_mode == GB_COLOR_CORRECTION_DISABLED || (for_border && !gb->has_sgb_border)) { + r = scale_channel(r); + g = scale_channel(g); + b = scale_channel(b); + } + else { + if (GB_is_sgb(gb) || for_border) { + return gb->rgb_encode_callback(gb, + scale_channel_with_curve_sgb(r), + scale_channel_with_curve_sgb(g), + scale_channel_with_curve_sgb(b)); + } + bool agb = gb->model == GB_MODEL_AGB; + r = agb? scale_channel_with_curve_agb(r) : scale_channel_with_curve(r); + g = agb? scale_channel_with_curve_agb(g) : scale_channel_with_curve(g); + b = agb? scale_channel_with_curve_agb(b) : scale_channel_with_curve(b); + + if (gb->color_correction_mode != GB_COLOR_CORRECTION_CORRECT_CURVES) { + uint8_t new_r, new_g, new_b; + if (agb) { + new_g = (g * 6 + b * 1) / 7; + } + else { + new_g = (g * 3 + b) / 4; + } + new_r = r; + new_b = b; + if (gb->color_correction_mode == GB_COLOR_CORRECTION_REDUCE_CONTRAST) { + r = new_r; + g = new_r; + b = new_r; + + new_r = new_r * 7 / 8 + ( g + b) / 16; + new_g = new_g * 7 / 8 + (r + b) / 16; + new_b = new_b * 7 / 8 + (r + g ) / 16; + + + new_r = new_r * (224 - 32) / 255 + 32; + new_g = new_g * (220 - 36) / 255 + 36; + new_b = new_b * (216 - 40) / 255 + 40; + } + else if (gb->color_correction_mode == GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS) { + uint8_t old_max = MAX(r, MAX(g, b)); + uint8_t new_max = MAX(new_r, MAX(new_g, new_b)); + + if (new_max != 0) { + new_r = new_r * old_max / new_max; + new_g = new_g * old_max / new_max; + new_b = new_b * old_max / new_max; + } + + uint8_t old_min = MIN(r, MIN(g, b)); + uint8_t new_min = MIN(new_r, MIN(new_g, new_b)); + + if (new_min != 0xff) { + new_r = 0xff - (0xff - new_r) * (0xff - old_min) / (0xff - new_min); + new_g = 0xff - (0xff - new_g) * (0xff - old_min) / (0xff - new_min); + new_b = 0xff - (0xff - new_b) * (0xff - old_min) / (0xff - new_min); + } + } + r = new_r; + g = new_g; + b = new_b; + } + } + + return gb->rgb_encode_callback(gb, r, g, b); +} + +void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index) +{ + if (!gb->rgb_encode_callback || !GB_is_cgb(gb)) return; + uint8_t *palette_data = background_palette? gb->background_palettes_data : gb->sprite_palettes_data; + uint16_t color = palette_data[index & ~1] | (palette_data[index | 1] << 8); + + (background_palette? gb->background_palettes_rgb : gb->sprite_palettes_rgb)[index / 2] = GB_convert_rgb15(gb, color, false); +} + +void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode) +{ + gb->color_correction_mode = mode; + if (GB_is_cgb(gb)) { + for (unsigned i = 0; i < 32; i++) { + GB_palette_changed(gb, false, i * 2); + GB_palette_changed(gb, true, i * 2); + } + } +} + +/* + STAT interrupt is implemented based on this finding: + http://board.byuu.org/phpbb3/viewtopic.php?p=25527#p25531 + + General timing is based on GiiBiiAdvance's documents: + https://github.com/AntonioND/giibiiadvance + + */ + +void GB_STAT_update(GB_gameboy_t *gb) +{ + if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) return; + + bool previous_interrupt_line = gb->stat_interrupt_line; + /* Set LY=LYC bit */ + /* TODO: This behavior might not be correct for CGB revisions other than C and E */ + if (gb->ly_for_comparison != (uint16_t)-1 || gb->model <= GB_MODEL_CGB_C) { + if (gb->ly_for_comparison == gb->io_registers[GB_IO_LYC]) { + gb->lyc_interrupt_line = true; + gb->io_registers[GB_IO_STAT] |= 4; + } + else { + if (gb->ly_for_comparison != (uint16_t)-1) { + gb->lyc_interrupt_line = false; + } + gb->io_registers[GB_IO_STAT] &= ~4; + } + } + + switch (gb->mode_for_interrupt) { + case 0: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 8; break; + case 1: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 0x10; break; + case 2: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 0x20; break; + default: gb->stat_interrupt_line = false; + } + + /* User requested a LY=LYC interrupt and the LY=LYC bit is on */ + if ((gb->io_registers[GB_IO_STAT] & 0x40) && gb->lyc_interrupt_line) { + gb->stat_interrupt_line = true; + } + + if (gb->stat_interrupt_line && !previous_interrupt_line) { + gb->io_registers[GB_IO_IF] |= 2; + } +} + +void GB_lcd_off(GB_gameboy_t *gb) +{ + gb->display_state = 0; + gb->display_cycles = 0; + /* When the LCD is disabled, state is constant */ + + /* When the LCD is off, LY is 0 and STAT mode is 0. */ + gb->io_registers[GB_IO_LY] = 0; + gb->io_registers[GB_IO_STAT] &= ~3; + if (gb->hdma_on_hblank) { + gb->hdma_on_hblank = false; + gb->hdma_on = false; + + /* Todo: is this correct? */ + gb->hdma_steps_left = 0xff; + } + + gb->oam_read_blocked = false; + gb->vram_read_blocked = false; + gb->oam_write_blocked = false; + gb->vram_write_blocked = false; + gb->cgb_palettes_blocked = false; + + gb->current_line = 0; + gb->ly_for_comparison = 0; + + gb->accessed_oam_row = -1; + gb->wy_triggered = false; +} + +static void add_object_from_index(GB_gameboy_t *gb, unsigned index) +{ + if (gb->n_visible_objs == 10) return; + + /* TODO: It appears that DMA blocks PPU access to OAM, but it needs verification. */ + if (gb->dma_steps_left && (gb->dma_cycles >= 0 || gb->is_dma_restarting)) { + return; + } + + if (gb->oam_ppu_blocked) { + return; + } + + /* This reverse sorts the visible objects by location and priority */ + GB_object_t *objects = (GB_object_t *) &gb->oam; + bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; + signed y = objects[index].y - 16; + if (y <= gb->current_line && y + (height_16? 16 : 8) > gb->current_line) { + unsigned j = 0; + for (; j < gb->n_visible_objs; j++) { + if (gb->obj_comparators[j] <= objects[index].x) break; + } + memmove(gb->visible_objs + j + 1, gb->visible_objs + j, gb->n_visible_objs - j); + memmove(gb->obj_comparators + j + 1, gb->obj_comparators + j, gb->n_visible_objs - j); + gb->visible_objs[j] = index; + gb->obj_comparators[j] = objects[index].x; + gb->n_visible_objs++; + } +} + +static void render_pixel_if_possible(GB_gameboy_t *gb) +{ + GB_fifo_item_t *fifo_item = NULL; + GB_fifo_item_t *oam_fifo_item = NULL; + bool draw_oam = false; + bool bg_enabled = true, bg_priority = false; + + if (fifo_size(&gb->bg_fifo)) { + fifo_item = fifo_pop(&gb->bg_fifo); + bg_priority = fifo_item->bg_priority; + + if (fifo_size(&gb->oam_fifo)) { + oam_fifo_item = fifo_pop(&gb->oam_fifo); + if (oam_fifo_item->pixel && (gb->io_registers[GB_IO_LCDC] & 2)) { + draw_oam = true; + bg_priority |= oam_fifo_item->bg_priority; + } + } + } + + + if (!fifo_item) return; + + /* Drop pixels for scrollings */ + if (gb->position_in_line >= 160 || (gb->disable_rendering && !gb->sgb)) { + gb->position_in_line++; + return; + } + + /* Mixing */ + + if ((gb->io_registers[GB_IO_LCDC] & 0x1) == 0) { + if (gb->cgb_mode) { + bg_priority = false; + } + else { + bg_enabled = false; + } + } + + uint8_t icd_pixel = 0; + uint32_t *dest = NULL; + if (!gb->sgb) { + if (gb->border_mode != GB_BORDER_ALWAYS) { + dest = gb->screen + gb->lcd_x + gb->current_line * WIDTH; + } + else { + dest = gb->screen + gb->lcd_x + gb->current_line * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH; + } + } + + { + uint8_t pixel = bg_enabled? fifo_item->pixel : 0; + if (pixel && bg_priority) { + draw_oam = false; + } + if (!gb->cgb_mode) { + pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3); + } + if (gb->sgb) { + if (gb->current_lcd_line < LINES) { + gb->sgb->screen_buffer[gb->lcd_x + gb->current_lcd_line * WIDTH] = gb->stopped? 0 : pixel; + } + } + else if (gb->model & GB_MODEL_NO_SFC_BIT) { + if (gb->icd_pixel_callback) { + icd_pixel = pixel; + } + } + else if (gb->cgb_palettes_ppu_blocked) { + *dest = gb->rgb_encode_callback(gb, 0, 0, 0); + } + else { + *dest = gb->background_palettes_rgb[fifo_item->palette * 4 + pixel]; + } + } + + if (draw_oam) { + uint8_t pixel = oam_fifo_item->pixel; + if (!gb->cgb_mode) { + /* Todo: Verify access timings */ + pixel = ((gb->io_registers[oam_fifo_item->palette? GB_IO_OBP1 : GB_IO_OBP0] >> (pixel << 1)) & 3); + } + if (gb->sgb) { + if (gb->current_lcd_line < LINES) { + gb->sgb->screen_buffer[gb->lcd_x + gb->current_lcd_line * WIDTH] = gb->stopped? 0 : pixel; + } + } + else if (gb->model & GB_MODEL_NO_SFC_BIT) { + if (gb->icd_pixel_callback) { + icd_pixel = pixel; + //gb->icd_pixel_callback(gb, pixel); + } + } + else if (gb->cgb_palettes_ppu_blocked) { + *dest = gb->rgb_encode_callback(gb, 0, 0, 0); + } + else { + *dest = gb->sprite_palettes_rgb[oam_fifo_item->palette * 4 + pixel]; + } + } + + if (gb->model & GB_MODEL_NO_SFC_BIT) { + if (gb->icd_pixel_callback) { + gb->icd_pixel_callback(gb, icd_pixel); + } + } + + gb->position_in_line++; + gb->lcd_x++; + gb->window_is_being_fetched = false; +} + +/* All verified CGB timings are based on CGB CPU E. CGB CPUs >= D are known to have + slightly different timings than CPUs <= C. + + Todo: Add support to CPU C and older */ + +static inline uint8_t fetcher_y(GB_gameboy_t *gb) +{ + return gb->wx_triggered? gb->window_y : gb->current_line + gb->io_registers[GB_IO_SCY]; +} + +static void advance_fetcher_state_machine(GB_gameboy_t *gb) +{ + typedef enum { + GB_FETCHER_GET_TILE, + GB_FETCHER_GET_TILE_DATA_LOWER, + GB_FETCHER_GET_TILE_DATA_HIGH, + GB_FETCHER_PUSH, + GB_FETCHER_SLEEP, + } fetcher_step_t; + + fetcher_step_t fetcher_state_machine [8] = { + GB_FETCHER_SLEEP, + GB_FETCHER_GET_TILE, + GB_FETCHER_SLEEP, + GB_FETCHER_GET_TILE_DATA_LOWER, + GB_FETCHER_SLEEP, + GB_FETCHER_GET_TILE_DATA_HIGH, + GB_FETCHER_PUSH, + GB_FETCHER_PUSH, + }; + switch (fetcher_state_machine[gb->fetcher_state & 7]) { + case GB_FETCHER_GET_TILE: { + uint16_t map = 0x1800; + + if (!(gb->io_registers[GB_IO_LCDC] & 0x20)) { + gb->wx_triggered = false; + gb->wx166_glitch = false; + } + + /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ + if (gb->io_registers[GB_IO_LCDC] & 0x08 && !gb->wx_triggered) { + map = 0x1C00; + } + else if (gb->io_registers[GB_IO_LCDC] & 0x40 && gb->wx_triggered) { + map = 0x1C00; + } + + /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ + uint8_t y = fetcher_y(gb); + uint8_t x = 0; + if (gb->wx_triggered) { + x = gb->window_tile_x; + } + else { + x = ((gb->io_registers[GB_IO_SCX] / 8) + gb->fetcher_x) & 0x1F; + } + if (gb->model > GB_MODEL_CGB_C) { + /* This value is cached on the CGB-D and newer, so it cannot be used to mix tiles together */ + gb->fetcher_y = y; + } + gb->last_tile_index_address = map + x + y / 8 * 32; + gb->current_tile = gb->vram[gb->last_tile_index_address]; + if (gb->vram_ppu_blocked) { + gb->current_tile = 0xFF; + } + if (GB_is_cgb(gb)) { + /* The CGB actually accesses both the tile index AND the attributes in the same T-cycle. + This probably means the CGB has a 16-bit data bus for the VRAM. */ + gb->current_tile_attributes = gb->vram[gb->last_tile_index_address + 0x2000]; + if (gb->vram_ppu_blocked) { + gb->current_tile_attributes = 0xFF; + } + } + } + gb->fetcher_state++; + break; + + case GB_FETCHER_GET_TILE_DATA_LOWER: { + uint8_t y_flip = 0; + uint16_t tile_address = 0; + uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb); + + /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ + if (gb->io_registers[GB_IO_LCDC] & 0x10) { + tile_address = gb->current_tile * 0x10; + } + else { + tile_address = (int8_t)gb->current_tile * 0x10 + 0x1000; + } + if (gb->current_tile_attributes & 8) { + tile_address += 0x2000; + } + if (gb->current_tile_attributes & 0x40) { + y_flip = 0x7; + } + gb->current_tile_data[0] = + gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; + if (gb->vram_ppu_blocked) { + gb->current_tile_data[0] = 0xFF; + } + } + gb->fetcher_state++; + break; + + case GB_FETCHER_GET_TILE_DATA_HIGH: { + /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. + Additionally, on CGB-D and newer mixing two tiles by changing the tileset + bit mid-fetching causes a glitched mixing of the two, in comparison to the + more logical DMG version. */ + uint16_t tile_address = 0; + uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb); + + if (gb->io_registers[GB_IO_LCDC] & 0x10) { + tile_address = gb->current_tile * 0x10; + } + else { + tile_address = (int8_t)gb->current_tile * 0x10 + 0x1000; + } + if (gb->current_tile_attributes & 8) { + tile_address += 0x2000; + } + uint8_t y_flip = 0; + if (gb->current_tile_attributes & 0x40) { + y_flip = 0x7; + } + gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2 + 1; + gb->current_tile_data[1] = + gb->vram[gb->last_tile_data_address]; + if (gb->vram_ppu_blocked) { + gb->current_tile_data[1] = 0xFF; + } + } + if (gb->wx_triggered) { + gb->window_tile_x++; + gb->window_tile_x &= 0x1f; + } + + // fallthrough + case GB_FETCHER_PUSH: { + if (gb->fetcher_state == 6) { + /* The background map index increase at this specific point. If this state is not reached, + it will simply not increase. */ + gb->fetcher_x++; + gb->fetcher_x &= 0x1f; + } + if (gb->fetcher_state < 7) { + gb->fetcher_state++; + } + if (fifo_size(&gb->bg_fifo) > 0) break; + + fifo_push_bg_row(&gb->bg_fifo, gb->current_tile_data[0], gb->current_tile_data[1], + gb->current_tile_attributes & 7, gb->current_tile_attributes & 0x80, gb->current_tile_attributes & 0x20); + gb->fetcher_state = 0; + } + break; + + case GB_FETCHER_SLEEP: + { + gb->fetcher_state++; + } + break; + } +} + +static uint16_t get_object_line_address(GB_gameboy_t *gb, const GB_object_t *object) +{ + /* TODO: what does the PPU read if DMA is active? */ + if (gb->oam_ppu_blocked) { + static const GB_object_t blocked = {0xFF, 0xFF, 0xFF, 0xFF}; + object = &blocked; + } + + bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; /* Todo: Which T-cycle actually reads this? */ + uint8_t tile_y = (gb->current_line - object->y) & (height_16? 0xF : 7); + + if (object->flags & 0x40) { /* Flip Y */ + tile_y ^= height_16? 0xF : 7; + } + + /* Todo: I'm not 100% sure an access to OAM can't trigger the OAM bug while we're accessing this */ + uint16_t line_address = (height_16? object->tile & 0xFE : object->tile) * 0x10 + tile_y * 2; + + if (gb->cgb_mode && (object->flags & 0x8)) { /* Use VRAM bank 2 */ + line_address += 0x2000; + } + return line_address; +} + +/* + TODO: It seems that the STAT register's mode bits are always "late" by 4 T-cycles. + The PPU logic can be greatly simplified if that delay is simply emulated. + */ +void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) +{ + /* The PPU does not advance while in STOP mode on the DMG */ + if (gb->stopped && !GB_is_cgb(gb)) { + gb->cycles_in_stop_mode += cycles; + if (gb->cycles_in_stop_mode >= LCDC_PERIOD) { + gb->cycles_in_stop_mode -= LCDC_PERIOD; + display_vblank(gb); + } + return; + } + GB_object_t *objects = (GB_object_t *) &gb->oam; + + GB_STATE_MACHINE(gb, display, cycles, 2) { + GB_STATE(gb, display, 1); + GB_STATE(gb, display, 2); + // GB_STATE(gb, display, 3); + // GB_STATE(gb, display, 4); + // GB_STATE(gb, display, 5); + GB_STATE(gb, display, 6); + GB_STATE(gb, display, 7); + GB_STATE(gb, display, 8); + // GB_STATE(gb, display, 9); + GB_STATE(gb, display, 10); + GB_STATE(gb, display, 11); + GB_STATE(gb, display, 12); + GB_STATE(gb, display, 13); + GB_STATE(gb, display, 14); + GB_STATE(gb, display, 15); + GB_STATE(gb, display, 16); + GB_STATE(gb, display, 17); + // GB_STATE(gb, display, 19); + GB_STATE(gb, display, 20); + GB_STATE(gb, display, 21); + GB_STATE(gb, display, 22); + GB_STATE(gb, display, 23); + // GB_STATE(gb, display, 24); + GB_STATE(gb, display, 25); + GB_STATE(gb, display, 26); + GB_STATE(gb, display, 27); + GB_STATE(gb, display, 28); + GB_STATE(gb, display, 29); + GB_STATE(gb, display, 30); + // GB_STATE(gb, display, 31); + GB_STATE(gb, display, 32); + GB_STATE(gb, display, 33); + GB_STATE(gb, display, 34); + GB_STATE(gb, display, 35); + GB_STATE(gb, display, 36); + GB_STATE(gb, display, 37); + GB_STATE(gb, display, 38); + GB_STATE(gb, display, 39); + GB_STATE(gb, display, 40); + GB_STATE(gb, display, 41); + GB_STATE(gb, display, 42); + } + + if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { + while (true) { + GB_SLEEP(gb, display, 1, LCDC_PERIOD); + display_vblank(gb); + gb->cgb_repeated_a_frame = true; + } + return; + } + + gb->is_odd_frame = false; + + if (!GB_is_cgb(gb)) { + GB_SLEEP(gb, display, 23, 1); + } + + /* Handle mode 2 on the very first line 0 */ + gb->current_line = 0; + gb->window_y = -1; + /* Todo: verify timings */ + if (gb->io_registers[GB_IO_WY] == 0) { + gb->wy_triggered = true; + } + else { + gb->wy_triggered = false; + } + + gb->ly_for_comparison = 0; + gb->io_registers[GB_IO_STAT] &= ~3; + gb->mode_for_interrupt = -1; + gb->oam_read_blocked = false; + gb->vram_read_blocked = false; + gb->oam_write_blocked = false; + gb->vram_write_blocked = false; + gb->cgb_palettes_blocked = false; + gb->cycles_for_line = MODE2_LENGTH - 4; + GB_STAT_update(gb); + GB_SLEEP(gb, display, 2, MODE2_LENGTH - 4); + + gb->oam_write_blocked = true; + gb->cycles_for_line += 2; + GB_STAT_update(gb); + GB_SLEEP(gb, display, 34, 2); + + gb->n_visible_objs = 0; + gb->cycles_for_line += 8; // Mode 0 is shorter on the first line 0, so we augment cycles_for_line by 8 extra cycles. + + gb->io_registers[GB_IO_STAT] &= ~3; + gb->io_registers[GB_IO_STAT] |= 3; + gb->mode_for_interrupt = 3; + + gb->oam_write_blocked = true; + gb->oam_read_blocked = true; + gb->vram_read_blocked = gb->cgb_double_speed; + gb->vram_write_blocked = gb->cgb_double_speed; + if (!GB_is_cgb(gb)) { + gb->vram_read_blocked = true; + gb->vram_write_blocked = true; + } + gb->cycles_for_line += 2; + GB_SLEEP(gb, display, 37, 2); + + gb->cgb_palettes_blocked = true; + gb->cycles_for_line += (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C)? 2 : 3; + GB_SLEEP(gb, display, 38, (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C)? 2 : 3); + + gb->vram_read_blocked = true; + gb->vram_write_blocked = true; + gb->wx_triggered = false; + gb->wx166_glitch = false; + goto mode_3_start; + + while (true) { + /* Lines 0 - 143 */ + gb->window_y = -1; + for (; gb->current_line < LINES; gb->current_line++) { + /* Todo: verify timings */ + if ((gb->io_registers[GB_IO_WY] == gb->current_line || + (gb->current_line != 0 && gb->io_registers[GB_IO_WY] == gb->current_line - 1))) { + gb->wy_triggered = true; + } + + gb->oam_write_blocked = GB_is_cgb(gb) && !gb->cgb_double_speed; + gb->accessed_oam_row = 0; + + GB_SLEEP(gb, display, 35, 2); + gb->oam_write_blocked = GB_is_cgb(gb); + + GB_SLEEP(gb, display, 6, 1); + gb->io_registers[GB_IO_LY] = gb->current_line; + gb->oam_read_blocked = true; + gb->ly_for_comparison = gb->current_line? -1 : 0; + + /* The OAM STAT interrupt occurs 1 T-cycle before STAT actually changes, except on line 0. + PPU glitch? */ + if (gb->current_line != 0) { + gb->mode_for_interrupt = 2; + gb->io_registers[GB_IO_STAT] &= ~3; + } + else if (!GB_is_cgb(gb)) { + gb->io_registers[GB_IO_STAT] &= ~3; + } + GB_STAT_update(gb); + + GB_SLEEP(gb, display, 7, 1); + + gb->io_registers[GB_IO_STAT] &= ~3; + gb->io_registers[GB_IO_STAT] |= 2; + gb->mode_for_interrupt = 2; + gb->oam_write_blocked = true; + gb->ly_for_comparison = gb->current_line; + GB_STAT_update(gb); + gb->mode_for_interrupt = -1; + GB_STAT_update(gb); + gb->n_visible_objs = 0; + + for (gb->oam_search_index = 0; gb->oam_search_index < 40; gb->oam_search_index++) { + if (GB_is_cgb(gb)) { + add_object_from_index(gb, gb->oam_search_index); + /* The CGB does not care about the accessed OAM row as there's no OAM bug */ + } + GB_SLEEP(gb, display, 8, 2); + if (!GB_is_cgb(gb)) { + add_object_from_index(gb, gb->oam_search_index); + gb->accessed_oam_row = (gb->oam_search_index & ~1) * 4 + 8; + } + if (gb->oam_search_index == 37) { + gb->vram_read_blocked = !GB_is_cgb(gb); + gb->vram_write_blocked = false; + gb->cgb_palettes_blocked = false; + gb->oam_write_blocked = GB_is_cgb(gb); + GB_STAT_update(gb); + } + } + gb->cycles_for_line = MODE2_LENGTH + 4; + + gb->accessed_oam_row = -1; + gb->io_registers[GB_IO_STAT] &= ~3; + gb->io_registers[GB_IO_STAT] |= 3; + gb->mode_for_interrupt = 3; + gb->vram_read_blocked = true; + gb->vram_write_blocked = true; + gb->cgb_palettes_blocked = false; + gb->oam_write_blocked = true; + gb->oam_read_blocked = true; + + GB_STAT_update(gb); + + + uint8_t idle_cycles = 3; + if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C) { + idle_cycles = 2; + } + gb->cycles_for_line += idle_cycles; + GB_SLEEP(gb, display, 10, idle_cycles); + + gb->cgb_palettes_blocked = true; + gb->cycles_for_line += 2; + GB_SLEEP(gb, display, 32, 2); + mode_3_start: + + fifo_clear(&gb->bg_fifo); + fifo_clear(&gb->oam_fifo); + /* Fill the FIFO with 8 pixels of "junk", it's going to be dropped anyway. */ + fifo_push_bg_row(&gb->bg_fifo, 0, 0, 0, false, false); + /* Todo: find out actual access time of SCX */ + gb->position_in_line = - (gb->io_registers[GB_IO_SCX] & 7) - 8; + gb->lcd_x = 0; + + gb->fetcher_x = 0; + gb->extra_penalty_for_sprite_at_0 = (gb->io_registers[GB_IO_SCX] & 7); + + + /* The actual rendering cycle */ + gb->fetcher_state = 0; + while (true) { + /* Handle window */ + /* TODO: It appears that WX checks if the window begins *next* pixel, not *this* pixel. For this reason, + WX=167 has no effect at all (It checks if the PPU X position is 161, which never happens) and WX=166 + has weird artifacts (It appears to activate the window during HBlank, as PPU X is temporarily 160 at + that point. The code should be updated to represent this, and this will fix the time travel hack in + WX's access conflict code. */ + + if (!gb->wx_triggered && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20)) { + bool should_activate_window = false; + if (gb->io_registers[GB_IO_WX] == 0) { + static const uint8_t scx_to_wx0_comparisons[] = {-7, -9, -10, -11, -12, -13, -14, -14}; + if (gb->position_in_line == scx_to_wx0_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { + should_activate_window = true; + } + } + else if (gb->wx166_glitch) { + static const uint8_t scx_to_wx166_comparisons[] = {-8, -9, -10, -11, -12, -13, -14, -15}; + if (gb->position_in_line == scx_to_wx166_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { + should_activate_window = true; + } + } + else if (gb->io_registers[GB_IO_WX] < 166 + GB_is_cgb(gb)) { + if (gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7)) { + should_activate_window = true; + } + else if (gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 6) && !gb->wx_just_changed) { + should_activate_window = true; + /* LCD-PPU horizontal desync! It only appears to happen on DMGs, but not all of them. + This doesn't seem to be CPU revision dependent, but most revisions */ + if ((gb->model & GB_MODEL_FAMILY_MASK) == GB_MODEL_DMG_FAMILY && !GB_is_sgb(gb)) { + if (gb->lcd_x > 0) { + gb->lcd_x--; + } + } + } + } + + if (should_activate_window) { + gb->window_y++; + /* TODO: Verify fetcher access timings in this case */ + if (gb->io_registers[GB_IO_WX] == 0 && (gb->io_registers[GB_IO_SCX] & 7)) { + gb->cycles_for_line++; + GB_SLEEP(gb, display, 42, 1); + } + gb->wx_triggered = true; + gb->window_tile_x = 0; + fifo_clear(&gb->bg_fifo); + gb->fetcher_state = 0; + gb->window_is_being_fetched = true; + } + else if (!GB_is_cgb(gb) && gb->io_registers[GB_IO_WX] == 166 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7)) { + gb->window_y++; + } + } + + /* TODO: What happens when WX=0? */ + if (!GB_is_cgb(gb) && gb->wx_triggered && !gb->window_is_being_fetched && + gb->fetcher_state == 0 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7) ) { + // Insert a pixel right at the FIFO's end + gb->bg_fifo.read_end--; + gb->bg_fifo.read_end &= GB_FIFO_LENGTH - 1; + gb->bg_fifo.fifo[gb->bg_fifo.read_end] = (GB_fifo_item_t){0,}; + gb->window_is_being_fetched = false; + } + + /* Handle objects */ + /* When the sprite enabled bit is off, this proccess is skipped entirely on the DMG, but not on the CGB. + On the CGB, this bit is checked only when the pixel is actually popped from the FIFO. */ + + while (gb->n_visible_objs != 0 && + (gb->position_in_line < 160 || gb->position_in_line >= (uint8_t)(-8)) && + gb->obj_comparators[gb->n_visible_objs - 1] < (uint8_t)(gb->position_in_line + 8)) { + gb->n_visible_objs--; + } + + gb->during_object_fetch = true; + while (gb->n_visible_objs != 0 && + (gb->io_registers[GB_IO_LCDC] & 2 || GB_is_cgb(gb)) && + gb->obj_comparators[gb->n_visible_objs - 1] == (uint8_t)(gb->position_in_line + 8)) { + + while (gb->fetcher_state < 5 || fifo_size(&gb->bg_fifo) == 0) { + advance_fetcher_state_machine(gb); + gb->cycles_for_line++; + GB_SLEEP(gb, display, 27, 1); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } + } + + /* Todo: Measure if penalty occurs before or after waiting for the fetcher. */ + if (gb->extra_penalty_for_sprite_at_0 != 0) { + if (gb->obj_comparators[gb->n_visible_objs - 1] == 0) { + gb->cycles_for_line += gb->extra_penalty_for_sprite_at_0; + GB_SLEEP(gb, display, 28, gb->extra_penalty_for_sprite_at_0); + gb->extra_penalty_for_sprite_at_0 = 0; + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } + } + } + + /* TODO: Can this be deleted? { */ + advance_fetcher_state_machine(gb); + gb->cycles_for_line++; + GB_SLEEP(gb, display, 41, 1); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } + /* } */ + + advance_fetcher_state_machine(gb); + + gb->cycles_for_line += 3; + GB_SLEEP(gb, display, 20, 3); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } + + gb->object_low_line_address = get_object_line_address(gb, &objects[gb->visible_objs[gb->n_visible_objs - 1]]); + + gb->cycles_for_line++; + GB_SLEEP(gb, display, 39, 1); + if (gb->object_fetch_aborted) { + goto abort_fetching_object; + } + + gb->during_object_fetch = false; + gb->cycles_for_line++; + GB_SLEEP(gb, display, 40, 1); + + const GB_object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; + + uint16_t line_address = get_object_line_address(gb, object); + + uint8_t palette = (object->flags & 0x10) ? 1 : 0; + if (gb->cgb_mode) { + palette = object->flags & 0x7; + } + fifo_overlay_object_row(&gb->oam_fifo, + gb->vram_ppu_blocked? 0xFF : gb->vram[gb->object_low_line_address], + gb->vram_ppu_blocked? 0xFF : gb->vram[line_address + 1], + palette, + object->flags & 0x80, + gb->object_priority == GB_OBJECT_PRIORITY_INDEX? gb->visible_objs[gb->n_visible_objs - 1] : 0, + object->flags & 0x20); + + gb->n_visible_objs--; + } + +abort_fetching_object: + gb->object_fetch_aborted = false; + gb->during_object_fetch = false; + + render_pixel_if_possible(gb); + advance_fetcher_state_machine(gb); + + if (gb->position_in_line == 160) break; + gb->cycles_for_line++; + GB_SLEEP(gb, display, 21, 1); + } + + while (gb->lcd_x != 160 && !gb->disable_rendering && gb->screen && !gb->sgb) { + /* Oh no! The PPU and LCD desynced! Fill the rest of the line whith white. */ + uint32_t *dest = NULL; + if (gb->border_mode != GB_BORDER_ALWAYS) { + dest = gb->screen + gb->lcd_x + gb->current_line * WIDTH; + } + else { + dest = gb->screen + gb->lcd_x + gb->current_line * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH; + } + *dest = gb->background_palettes_rgb[0]; + gb->lcd_x++; + + } + + /* TODO: Verify timing */ + if (!GB_is_cgb(gb) && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20) && gb->io_registers[GB_IO_WX] == 166) { + gb->wx166_glitch = true; + } + else { + gb->wx166_glitch = false; + } + gb->wx_triggered = false; + + if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C) { + gb->cycles_for_line++; + GB_SLEEP(gb, display, 30, 1); + } + + if (!gb->cgb_double_speed) { + gb->io_registers[GB_IO_STAT] &= ~3; + gb->mode_for_interrupt = 0; + gb->oam_read_blocked = false; + gb->vram_read_blocked = false; + gb->oam_write_blocked = false; + gb->vram_write_blocked = false; + } + + gb->cycles_for_line++; + GB_SLEEP(gb, display, 22, 1); + + gb->io_registers[GB_IO_STAT] &= ~3; + gb->mode_for_interrupt = 0; + gb->oam_read_blocked = false; + gb->vram_read_blocked = false; + gb->oam_write_blocked = false; + gb->vram_write_blocked = false; + GB_STAT_update(gb); + + /* Todo: Measure this value */ + gb->cycles_for_line += 2; + GB_SLEEP(gb, display, 33, 2); + gb->cgb_palettes_blocked = !gb->cgb_double_speed; + + gb->cycles_for_line += 2; + GB_SLEEP(gb, display, 36, 2); + gb->cgb_palettes_blocked = false; + + gb->cycles_for_line += 8; + GB_SLEEP(gb, display, 25, 8); + + if (gb->hdma_on_hblank) { + gb->hdma_starting = true; + } + GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line); + gb->mode_for_interrupt = 2; + + // Todo: unverified timing + gb->current_lcd_line++; + if (gb->current_lcd_line == LINES && GB_is_sgb(gb)) { + display_vblank(gb); + } + + if (gb->icd_hreset_callback) { + gb->icd_hreset_callback(gb); + } + } + gb->wx166_glitch = false; + /* Lines 144 - 152 */ + for (; gb->current_line < VIRTUAL_LINES - 1; gb->current_line++) { + gb->io_registers[GB_IO_LY] = gb->current_line; + gb->ly_for_comparison = -1; + GB_SLEEP(gb, display, 26, 2); + if (gb->current_line == LINES) { + gb->mode_for_interrupt = 2; + } + GB_STAT_update(gb); + GB_SLEEP(gb, display, 12, 2); + gb->ly_for_comparison = gb->current_line; + + if (gb->current_line == LINES) { + /* Entering VBlank state triggers the OAM interrupt */ + gb->io_registers[GB_IO_STAT] &= ~3; + gb->io_registers[GB_IO_STAT] |= 1; + gb->io_registers[GB_IO_IF] |= 1; + gb->mode_for_interrupt = 2; + GB_STAT_update(gb); + gb->mode_for_interrupt = 1; + GB_STAT_update(gb); + + if (gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) { + if (GB_is_cgb(gb)) { + GB_timing_sync(gb); + gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_SKIPPED; + } + else { + if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) { + gb->is_odd_frame ^= true; + display_vblank(gb); + } + gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + } + } + else { + if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) { + gb->is_odd_frame ^= true; + display_vblank(gb); + } + if (gb->frame_skip_state == GB_FRAMESKIP_FIRST_FRAME_SKIPPED) { + gb->cgb_repeated_a_frame = true; + gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + } + else { + gb->cgb_repeated_a_frame = false; + } + } + } + + GB_STAT_update(gb); + GB_SLEEP(gb, display, 13, LINE_LENGTH - 4); + } + + /* TODO: Verified on SGB2 and CGB-E. Actual interrupt timings not tested. */ + /* Lines 153 */ + gb->io_registers[GB_IO_LY] = 153; + gb->ly_for_comparison = -1; + GB_STAT_update(gb); + GB_SLEEP(gb, display, 14, (gb->model > GB_MODEL_CGB_C)? 4: 6); + + if (!GB_is_cgb(gb)) { + gb->io_registers[GB_IO_LY] = 0; + } + gb->ly_for_comparison = 153; + GB_STAT_update(gb); + GB_SLEEP(gb, display, 15, (gb->model > GB_MODEL_CGB_C)? 4: 2); + + gb->io_registers[GB_IO_LY] = 0; + gb->ly_for_comparison = (gb->model > GB_MODEL_CGB_C)? 153 : -1; + GB_STAT_update(gb); + GB_SLEEP(gb, display, 16, 4); + + gb->ly_for_comparison = 0; + GB_STAT_update(gb); + GB_SLEEP(gb, display, 29, 12); /* Writing to LYC during this period on a CGB has side effects */ + GB_SLEEP(gb, display, 17, LINE_LENGTH - 24); + + + gb->current_line = 0; + /* Todo: verify timings */ + if ((gb->io_registers[GB_IO_LCDC] & 0x20) && + (gb->io_registers[GB_IO_WY] == 0)) { + gb->wy_triggered = true; + } + else { + gb->wy_triggered = false; + } + + // TODO: not the correct timing + gb->current_lcd_line = 0; + if (gb->icd_vreset_callback) { + gb->icd_vreset_callback(gb); + } + } +} + +void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index) +{ + uint32_t none_palette[4]; + uint32_t *palette = NULL; + + switch (GB_is_cgb(gb)? palette_type : GB_PALETTE_NONE) { + default: + case GB_PALETTE_NONE: + none_palette[0] = gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF); + none_palette[1] = gb->rgb_encode_callback(gb, 0xAA, 0xAA, 0xAA); + none_palette[2] = gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55); + none_palette[3] = gb->rgb_encode_callback(gb, 0, 0, 0 ); + palette = none_palette; + break; + case GB_PALETTE_BACKGROUND: + palette = gb->background_palettes_rgb + (4 * (palette_index & 7)); + break; + case GB_PALETTE_OAM: + palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7)); + break; + } + + for (unsigned y = 0; y < 192; y++) { + for (unsigned x = 0; x < 256; x++) { + if (x >= 128 && !GB_is_cgb(gb)) { + *(dest++) = gb->background_palettes_rgb[0]; + continue; + } + uint16_t tile = (x % 128) / 8 + y / 8 * 16; + uint16_t tile_address = tile * 0x10 + (x >= 128? 0x2000 : 0); + uint8_t pixel = (((gb->vram[tile_address + (y & 7) * 2 ] >> ((~x)&7)) & 1 ) | + ((gb->vram[tile_address + (y & 7) * 2 + 1] >> ((~x)&7)) & 1) << 1); + + if (!gb->cgb_mode) { + if (palette_type == GB_PALETTE_BACKGROUND) { + pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3); + } + else if (!gb->cgb_mode) { + if (palette_type == GB_PALETTE_OAM) { + pixel = ((gb->io_registers[palette_index == 0? GB_IO_OBP0 : GB_IO_OBP1] >> (pixel << 1)) & 3); + } + } + } + + + *(dest++) = palette[pixel]; + } + } +} + +void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index, GB_map_type_t map_type, GB_tileset_type_t tileset_type) +{ + uint32_t none_palette[4]; + uint32_t *palette = NULL; + uint16_t map = 0x1800; + + switch (GB_is_cgb(gb)? palette_type : GB_PALETTE_NONE) { + case GB_PALETTE_NONE: + none_palette[0] = gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF); + none_palette[1] = gb->rgb_encode_callback(gb, 0xAA, 0xAA, 0xAA); + none_palette[2] = gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55); + none_palette[3] = gb->rgb_encode_callback(gb, 0, 0, 0 ); + palette = none_palette; + break; + case GB_PALETTE_BACKGROUND: + palette = gb->background_palettes_rgb + (4 * (palette_index & 7)); + break; + case GB_PALETTE_OAM: + palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7)); + break; + case GB_PALETTE_AUTO: + break; + } + + if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && gb->io_registers[GB_IO_LCDC] & 0x08)) { + map = 0x1c00; + } + + if (tileset_type == GB_TILESET_AUTO) { + tileset_type = (gb->io_registers[GB_IO_LCDC] & 0x10)? GB_TILESET_8800 : GB_TILESET_8000; + } + + for (unsigned y = 0; y < 256; y++) { + for (unsigned x = 0; x < 256; x++) { + uint8_t tile = gb->vram[map + x/8 + y/8 * 32]; + uint16_t tile_address; + uint8_t attributes = 0; + + if (tileset_type == GB_TILESET_8800) { + tile_address = tile * 0x10; + } + else { + tile_address = (int8_t) tile * 0x10 + 0x1000; + } + + if (gb->cgb_mode) { + attributes = gb->vram[map + x/8 + y/8 * 32 + 0x2000]; + } + + if (attributes & 0x8) { + tile_address += 0x2000; + } + + uint8_t pixel = (((gb->vram[tile_address + (((attributes & 0x40)? ~y : y) & 7) * 2 ] >> (((attributes & 0x20)? x : ~x)&7)) & 1 ) | + ((gb->vram[tile_address + (((attributes & 0x40)? ~y : y) & 7) * 2 + 1] >> (((attributes & 0x20)? x : ~x)&7)) & 1) << 1); + + if (!gb->cgb_mode && (palette_type == GB_PALETTE_BACKGROUND || palette_type == GB_PALETTE_AUTO)) { + pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3); + } + + if (palette) { + *(dest++) = palette[pixel]; + } + else { + *(dest++) = gb->background_palettes_rgb[(attributes & 7) * 4 + pixel]; + } + } + } +} + +uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height) +{ + uint8_t count = 0; + *sprite_height = (gb->io_registers[GB_IO_LCDC] & 4) ? 16:8; + uint8_t oam_to_dest_index[40] = {0,}; + for (unsigned y = 0; y < LINES; y++) { + GB_object_t *sprite = (GB_object_t *) &gb->oam; + uint8_t sprites_in_line = 0; + for (uint8_t i = 0; i < 40; i++, sprite++) { + signed sprite_y = sprite->y - 16; + bool obscured = false; + // Is sprite not in this line? + if (sprite_y > y || sprite_y + *sprite_height <= y) continue; + if (++sprites_in_line == 11) obscured = true; + + GB_oam_info_t *info = NULL; + if (!oam_to_dest_index[i]) { + info = dest + count; + oam_to_dest_index[i] = ++count; + info->x = sprite->x; + info->y = sprite->y; + info->tile = *sprite_height == 16? sprite->tile & 0xFE : sprite->tile; + info->flags = sprite->flags; + info->obscured_by_line_limit = false; + info->oam_addr = 0xFE00 + i * sizeof(*sprite); + } + else { + info = dest + oam_to_dest_index[i] - 1; + } + info->obscured_by_line_limit |= obscured; + } + } + + for (unsigned i = 0; i < count; i++) { + uint16_t vram_address = dest[i].tile * 0x10; + uint8_t flags = dest[i].flags; + uint8_t palette = gb->cgb_mode? (flags & 7) : ((flags & 0x10)? 1 : 0); + if (GB_is_cgb(gb) && (flags & 0x8)) { + vram_address += 0x2000; + } + + for (unsigned y = 0; y < *sprite_height; y++) { + UNROLL + for (unsigned x = 0; x < 8; x++) { + uint8_t color = (((gb->vram[vram_address ] >> ((~x)&7)) & 1 ) | + ((gb->vram[vram_address + 1] >> ((~x)&7)) & 1) << 1 ); + + if (!gb->cgb_mode) { + color = (gb->io_registers[palette? GB_IO_OBP1:GB_IO_OBP0] >> (color << 1)) & 3; + } + dest[i].image[((flags & 0x20)?7-x:x) + ((flags & 0x40)?*sprite_height - 1 -y:y) * 8] = gb->sprite_palettes_rgb[palette * 4 + color]; + } + vram_address += 2; + } + } + return count; +} + + +bool GB_is_odd_frame(GB_gameboy_t *gb) +{ + return gb->is_odd_frame; +} diff --git a/bsnes/gb/Core/display.h b/bsnes/gb/Core/display.h new file mode 100644 index 00000000..5bdeba8d --- /dev/null +++ b/bsnes/gb/Core/display.h @@ -0,0 +1,62 @@ +#ifndef display_h +#define display_h + +#include "gb.h" +#include +#include + +#ifdef GB_INTERNAL +void GB_display_run(GB_gameboy_t *gb, uint8_t cycles); +void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index); +void GB_STAT_update(GB_gameboy_t *gb); +void GB_lcd_off(GB_gameboy_t *gb); + +enum { + GB_OBJECT_PRIORITY_UNDEFINED, // For save state compatibility + GB_OBJECT_PRIORITY_X, + GB_OBJECT_PRIORITY_INDEX, +}; + +#endif + +typedef enum { + GB_PALETTE_NONE, + GB_PALETTE_BACKGROUND, + GB_PALETTE_OAM, + GB_PALETTE_AUTO, +} GB_palette_type_t; + +typedef enum { + GB_MAP_AUTO, + GB_MAP_9800, + GB_MAP_9C00, +} GB_map_type_t; + +typedef enum { + GB_TILESET_AUTO, + GB_TILESET_8800, + GB_TILESET_8000, +} GB_tileset_type_t; + +typedef struct { + uint32_t image[128]; + uint8_t x, y, tile, flags; + uint16_t oam_addr; + bool obscured_by_line_limit; +} GB_oam_info_t; + +typedef enum { + GB_COLOR_CORRECTION_DISABLED, + GB_COLOR_CORRECTION_CORRECT_CURVES, + GB_COLOR_CORRECTION_EMULATE_HARDWARE, + GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS, + GB_COLOR_CORRECTION_REDUCE_CONTRAST, +} GB_color_correction_mode_t; + +void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index); +void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index, GB_map_type_t map_type, GB_tileset_type_t tileset_type); +uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height); +uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border); +void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode); +bool GB_is_odd_frame(GB_gameboy_t *gb); +#endif /* display_h */ diff --git a/bsnes/gb/Core/gb.c b/bsnes/gb/Core/gb.c new file mode 100644 index 00000000..7325d79a --- /dev/null +++ b/bsnes/gb/Core/gb.c @@ -0,0 +1,1619 @@ +#include +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#include +#endif +#include "random.h" +#include "gb.h" + + +#ifdef GB_DISABLE_REWIND +#define GB_rewind_free(...) +#define GB_rewind_push(...) +#endif + + +static inline uint32_t state_magic(void) +{ + if (sizeof(bool) == 1) return 'SAME'; + return 'S4ME'; +} + +void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, va_list args) +{ + char *string = NULL; + vasprintf(&string, fmt, args); + if (string) { + if (gb->log_callback) { + gb->log_callback(gb, string, attributes); + } + else { + /* Todo: Add ANSI escape sequences for attributed text */ + printf("%s", string); + } + } + free(string); +} + +void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + GB_attributed_logv(gb, attributes, fmt, args); + va_end(args); +} + +void GB_log(GB_gameboy_t *gb, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + GB_attributed_logv(gb, 0, fmt, args); + va_end(args); +} + +#ifndef GB_DISABLE_DEBUGGER +static char *default_input_callback(GB_gameboy_t *gb) +{ + char *expression = NULL; + size_t size = 0; + if (gb->debug_stopped) { + printf(">"); + } + + if (getline(&expression, &size, stdin) == -1) { + /* The user doesn't have STDIN or used ^D. We make sure the program keeps running. */ + GB_set_async_input_callback(gb, NULL); /* Disable async input */ + return strdup("c"); + } + + if (!expression) { + return strdup(""); + } + + size_t length = strlen(expression); + if (expression[length - 1] == '\n') { + expression[length - 1] = 0; + } + + if (expression[0] == '\x03') { + gb->debug_stopped = true; + free(expression); + return strdup(""); + } + return expression; +} + +static char *default_async_input_callback(GB_gameboy_t *gb) +{ +#ifndef _WIN32 + fd_set set; + FD_ZERO(&set); + FD_SET(STDIN_FILENO, &set); + struct timeval time = {0,}; + if (select(1, &set, NULL, NULL, &time) == 1) { + if (feof(stdin)) { + GB_set_async_input_callback(gb, NULL); /* Disable async input */ + return NULL; + } + return default_input_callback(gb); + } +#endif + return NULL; +} +#endif + +static void load_default_border(GB_gameboy_t *gb) +{ + if (gb->has_sgb_border) return; + + #define LOAD_BORDER() do { \ + memcpy(gb->borrowed_border.map, tilemap, sizeof(tilemap));\ + memcpy(gb->borrowed_border.palette, palette, sizeof(palette));\ + \ + /* Expand tileset */\ + for (unsigned tile = 0; tile < sizeof(tiles) / 32; tile++) {\ + for (unsigned y = 0; y < 8; y++) {\ + for (unsigned x = 0; x < 8; x++) {\ + gb->borrowed_border.tiles[tile * 8 * 8 + y * 8 + x] =\ + (tiles[tile * 32 + y * 2 + 0] & (1 << (7 ^ x)) ? 1 : 0) |\ + (tiles[tile * 32 + y * 2 + 1] & (1 << (7 ^ x)) ? 2 : 0) |\ + (tiles[tile * 32 + y * 2 + 16] & (1 << (7 ^ x)) ? 4 : 0) |\ + (tiles[tile * 32 + y * 2 + 17] & (1 << (7 ^ x)) ? 8 : 0);\ + }\ + }\ + }\ + } while (false); + + if (gb->model == GB_MODEL_AGB) { + #include "graphics/agb_border.inc" + LOAD_BORDER(); + } + else if (GB_is_cgb(gb)) { + #include "graphics/cgb_border.inc" + LOAD_BORDER(); + } + else { + #include "graphics/dmg_border.inc" + LOAD_BORDER(); + } +} + +void GB_init(GB_gameboy_t *gb, GB_model_t model) +{ + memset(gb, 0, sizeof(*gb)); + gb->model = model; + if (GB_is_cgb(gb)) { + gb->ram = malloc(gb->ram_size = 0x1000 * 8); + gb->vram = malloc(gb->vram_size = 0x2000 * 2); + } + else { + gb->ram = malloc(gb->ram_size = 0x2000); + gb->vram = malloc(gb->vram_size = 0x2000); + } + +#ifndef GB_DISABLE_DEBUGGER + gb->input_callback = default_input_callback; + gb->async_input_callback = default_async_input_callback; +#endif + gb->cartridge_type = &GB_cart_defs[0]; // Default cartridge type + gb->clock_multiplier = 1.0; + + if (model & GB_MODEL_NO_SFC_BIT) { + /* Disable time syncing. Timing should be done by the SFC emulator. */ + gb->turbo = true; + } + + GB_reset(gb); + load_default_border(gb); +} + +GB_model_t GB_get_model(GB_gameboy_t *gb) +{ + return gb->model; +} + +void GB_free(GB_gameboy_t *gb) +{ + gb->magic = 0; + if (gb->ram) { + free(gb->ram); + } + if (gb->vram) { + free(gb->vram); + } + if (gb->mbc_ram) { + free(gb->mbc_ram); + } + if (gb->rom) { + free(gb->rom); + } + if (gb->breakpoints) { + free(gb->breakpoints); + } + if (gb->sgb) { + free(gb->sgb); + } + if (gb->nontrivial_jump_state) { + free(gb->nontrivial_jump_state); + } +#ifndef GB_DISABLE_DEBUGGER + GB_debugger_clear_symbols(gb); +#endif + GB_rewind_free(gb); +#ifndef GB_DISABLE_CHEATS + while (gb->cheats) { + GB_remove_cheat(gb, gb->cheats[0]); + } +#endif + memset(gb, 0, sizeof(*gb)); +} + +int GB_load_boot_rom(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + GB_log(gb, "Could not open boot ROM: %s.\n", strerror(errno)); + return errno; + } + fread(gb->boot_rom, sizeof(gb->boot_rom), 1, f); + fclose(f); + return 0; +} + +void GB_load_boot_rom_from_buffer(GB_gameboy_t *gb, const unsigned char *buffer, size_t size) +{ + if (size > sizeof(gb->boot_rom)) { + size = sizeof(gb->boot_rom); + } + memset(gb->boot_rom, 0xFF, sizeof(gb->boot_rom)); + memcpy(gb->boot_rom, buffer, size); +} + +void GB_borrow_sgb_border(GB_gameboy_t *gb) +{ + if (GB_is_sgb(gb)) return; + if (gb->border_mode != GB_BORDER_ALWAYS) return; + if (gb->tried_loading_sgb_border) return; + gb->tried_loading_sgb_border = true; + if (gb->rom && gb->rom[0x146] != 3) return; // Not an SGB game, nothing to borrow + if (!gb->boot_rom_load_callback) return; // Can't borrow a border without this callback + GB_gameboy_t sgb; + GB_init(&sgb, GB_MODEL_SGB); + sgb.cartridge_type = gb->cartridge_type; + sgb.rom = gb->rom; + sgb.rom_size = gb->rom_size; + sgb.turbo = true; + sgb.turbo_dont_skip = true; + // sgb.disable_rendering = true; + + /* Load the boot ROM using the existing gb object */ + typeof(gb->boot_rom) boot_rom_backup; + memcpy(boot_rom_backup, gb->boot_rom, sizeof(gb->boot_rom)); + gb->boot_rom_load_callback(gb, GB_BOOT_ROM_SGB); + memcpy(sgb.boot_rom, gb->boot_rom, sizeof(gb->boot_rom)); + memcpy(gb->boot_rom, boot_rom_backup, sizeof(gb->boot_rom)); + sgb.sgb->intro_animation = -1; + + for (unsigned i = 600; i--;) { + GB_run_frame(&sgb); + if (sgb.sgb->border_animation) { + gb->has_sgb_border = true; + memcpy(&gb->borrowed_border, &sgb.sgb->pending_border, sizeof(gb->borrowed_border)); + gb->borrowed_border.palette[0] = sgb.sgb->effective_palettes[0]; + break; + } + } + + sgb.rom = NULL; + sgb.rom_size = 0; + GB_free(&sgb); +} + +int GB_load_rom(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + GB_log(gb, "Could not open ROM: %s.\n", strerror(errno)); + return errno; + } + fseek(f, 0, SEEK_END); + gb->rom_size = (ftell(f) + 0x3FFF) & ~0x3FFF; /* Round to bank */ + /* And then round to a power of two */ + while (gb->rom_size & (gb->rom_size - 1)) { + /* I promise this works. */ + gb->rom_size |= gb->rom_size >> 1; + gb->rom_size++; + } + if (gb->rom_size == 0) { + gb->rom_size = 0x8000; + } + fseek(f, 0, SEEK_SET); + if (gb->rom) { + free(gb->rom); + } + gb->rom = malloc(gb->rom_size); + memset(gb->rom, 0xFF, gb->rom_size); /* Pad with 0xFFs */ + fread(gb->rom, 1, gb->rom_size, f); + fclose(f); + GB_configure_cart(gb); + return 0; +} + +int GB_load_isx(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + GB_log(gb, "Could not open ISX file: %s.\n", strerror(errno)); + return errno; + } + char magic[4]; +#define READ(x) if (fread(&x, sizeof(x), 1, f) != 1) goto error + fread(magic, 1, sizeof(magic), f); + +#ifdef GB_BIG_ENDIAN + bool extended = *(uint32_t *)&magic == 'ISX '; +#else + bool extended = *(uint32_t *)&magic == __builtin_bswap32('ISX '); +#endif + + fseek(f, extended? 0x20 : 0, SEEK_SET); + + + uint8_t *old_rom = gb->rom; + uint32_t old_size = gb->rom_size; + gb->rom = NULL; + gb->rom_size = 0; + + while (true) { + uint8_t record_type = 0; + if (fread(&record_type, sizeof(record_type), 1, f) != 1) break; + switch (record_type) { + case 0x01: { // Binary + uint16_t bank; + uint16_t address; + uint16_t length; + uint8_t byte; + READ(byte); + bank = byte; + if (byte >= 0x80) { + READ(byte); + bank |= byte << 8; + } + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap16(address); +#endif + address &= 0x3FFF; + + READ(length); +#ifdef GB_BIG_ENDIAN + length = __builtin_bswap16(length); +#endif + + size_t needed_size = bank * 0x4000 + address + length; + if (needed_size > 1024 * 1024 * 32) goto error; + + if (gb->rom_size < needed_size) { + gb->rom = realloc(gb->rom, needed_size); + memset(gb->rom + gb->rom_size, 0, needed_size - gb->rom_size); + gb->rom_size = needed_size; + } + + if (fread(gb->rom + (bank * 0x4000 + address), length, 1, f) != 1) goto error; + + break; + } + + case 0x11: { // Extended Binary + uint32_t address; + uint32_t length; + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap32(address); +#endif + + READ(length); +#ifdef GB_BIG_ENDIAN + length = __builtin_bswap32(length); +#endif + size_t needed_size = address + length; + if (needed_size > 1024 * 1024 * 32) goto error; + + if (gb->rom_size < needed_size) { + gb->rom = realloc(gb->rom, needed_size); + memset(gb->rom + gb->rom_size, 0, needed_size - gb->rom_size); + gb->rom_size = needed_size; + } + + if (fread(gb->rom + address, length, 1, f) != 1) goto error; + + break; + } + + case 0x04: { // Symbol + uint16_t count; + uint8_t length; + char name[257]; + uint8_t flag; + uint16_t bank; + uint16_t address; + uint8_t byte; + READ(count); +#ifdef GB_BIG_ENDIAN + count = __builtin_bswap16(count); +#endif + while (count--) { + READ(length); + if (fread(name, length, 1, f) != 1) goto error; + name[length] = 0; + READ(flag); // unused + + READ(byte); + bank = byte; + if (byte >= 0x80) { + READ(byte); + bank |= byte << 8; + } + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap16(address); +#endif + GB_debugger_add_symbol(gb, bank, address, name); + } + break; + } + + case 0x14: { // Extended Binary + uint16_t count; + uint8_t length; + char name[257]; + uint8_t flag; + uint32_t address; + READ(count); +#ifdef GB_BIG_ENDIAN + count = __builtin_bswap16(count); +#endif + while (count--) { + READ(length); + if (fread(name, length + 1, 1, f) != 1) goto error; + name[length] = 0; + READ(flag); // unused + + READ(address); +#ifdef GB_BIG_ENDIAN + address = __builtin_bswap32(address); +#endif + // TODO: How to convert 32-bit addresses to Bank:Address? Needs to tell RAM and ROM apart + } + break; + } + + default: + goto done; + } + } +done:; +#undef READ + if (gb->rom_size == 0) goto error; + + size_t needed_size = (gb->rom_size + 0x3FFF) & ~0x3FFF; /* Round to bank */ + + /* And then round to a power of two */ + while (needed_size & (needed_size - 1)) { + /* I promise this works. */ + needed_size |= needed_size >> 1; + needed_size++; + } + + if (needed_size < 0x8000) { + needed_size = 0x8000; + } + + if (gb->rom_size < needed_size) { + gb->rom = realloc(gb->rom, needed_size); + memset(gb->rom + gb->rom_size, 0, needed_size - gb->rom_size); + gb->rom_size = needed_size; + } + + GB_configure_cart(gb); + + // Fix a common wrong MBC error + if (gb->rom[0x147] == 3) { // MBC1 + RAM + Battery + bool needs_fix = false; + if (gb->rom_size >= 0x21 * 0x4000) { + for (unsigned i = 0x20 * 0x4000; i < 0x21 * 0x4000; i++) { + if (gb->rom[i]) { + needs_fix = true; + break; + } + } + } + if (!needs_fix && gb->rom_size >= 0x41 * 0x4000) { + for (unsigned i = 0x40 * 0x4000; i < 0x41 * 0x4000; i++) { + if (gb->rom[i]) { + needs_fix = true; + break; + } + } + } + if (!needs_fix && gb->rom_size >= 0x61 * 0x4000) { + for (unsigned i = 0x60 * 0x4000; i < 0x61 * 0x4000; i++) { + if (gb->rom[i]) { + needs_fix = true; + break; + } + } + } + if (needs_fix) { + gb->rom[0x147] = 0x10; // MBC3 + RTC + RAM + Battery + GB_configure_cart(gb); + gb->rom[0x147] = 0x3; + GB_log(gb, "ROM claims to use MBC1 but appears to require MBC3 or 5, assuming MBC3.\n"); + } + } + + if (old_rom) { + free(old_rom); + } + + return 0; +error: + GB_log(gb, "Invalid or unsupported ISX file.\n"); + if (gb->rom) { + free(gb->rom); + gb->rom = old_rom; + gb->rom_size = old_size; + } + fclose(f); + return -1; +} + +void GB_load_rom_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size) +{ + gb->rom_size = (size + 0x3fff) & ~0x3fff; + while (gb->rom_size & (gb->rom_size - 1)) { + gb->rom_size |= gb->rom_size >> 1; + gb->rom_size++; + } + if (gb->rom_size == 0) { + gb->rom_size = 0x8000; + } + if (gb->rom) { + free(gb->rom); + } + gb->rom = malloc(gb->rom_size); + memset(gb->rom, 0xff, gb->rom_size); + memcpy(gb->rom, buffer, size); + GB_configure_cart(gb); +} + +typedef struct { + uint8_t seconds; + uint8_t padding1[3]; + uint8_t minutes; + uint8_t padding2[3]; + uint8_t hours; + uint8_t padding3[3]; + uint8_t days; + uint8_t padding4[3]; + uint8_t high; + uint8_t padding5[3]; +} GB_vba_rtc_time_t; + +typedef struct __attribute__((packed)) { + uint64_t last_rtc_second; + uint16_t minutes; + uint16_t days; + uint16_t alarm_minutes, alarm_days; + uint8_t alarm_enabled; +} GB_huc3_rtc_time_t; + +typedef union { + struct __attribute__((packed)) { + GB_rtc_time_t rtc_real; + time_t last_rtc_second; /* Platform specific endianess and size */ + } sameboy_legacy; + struct { + /* Used by VBA versions with 32-bit timestamp*/ + GB_vba_rtc_time_t rtc_real, rtc_latched; + uint32_t last_rtc_second; /* Always little endian */ + } vba32; + struct { + /* Used by BGB and VBA versions with 64-bit timestamp*/ + GB_vba_rtc_time_t rtc_real, rtc_latched; + uint64_t last_rtc_second; /* Always little endian */ + } vba64; +} GB_rtc_save_t; + +int GB_save_battery_size(GB_gameboy_t *gb) +{ + if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. + if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */ + + if (gb->cartridge_type->mbc_type == GB_HUC3) { + return gb->mbc_ram_size + sizeof(GB_huc3_rtc_time_t); + } + GB_rtc_save_t rtc_save_size; + return gb->mbc_ram_size + (gb->cartridge_type->has_rtc ? sizeof(rtc_save_size.vba64) : 0); +} + +int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) +{ + if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. + if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */ + + if (size < GB_save_battery_size(gb)) return EIO; + + memcpy(buffer, gb->mbc_ram, gb->mbc_ram_size); + + if (gb->cartridge_type->mbc_type == GB_HUC3) { + buffer += gb->mbc_ram_size; + +#ifdef GB_BIG_ENDIAN + GB_huc3_rtc_time_t rtc_save = { + __builtin_bswap64(gb->last_rtc_second), + __builtin_bswap16(gb->huc3_minutes), + __builtin_bswap16(gb->huc3_days), + __builtin_bswap16(gb->huc3_alarm_minutes), + __builtin_bswap16(gb->huc3_alarm_days), + gb->huc3_alarm_enabled, + }; +#else + GB_huc3_rtc_time_t rtc_save = { + gb->last_rtc_second, + gb->huc3_minutes, + gb->huc3_days, + gb->huc3_alarm_minutes, + gb->huc3_alarm_days, + gb->huc3_alarm_enabled, + }; +#endif + memcpy(buffer, &rtc_save, sizeof(rtc_save)); + } + else if (gb->cartridge_type->has_rtc) { + GB_rtc_save_t rtc_save = {{{{0,}},},}; + rtc_save.vba64.rtc_real.seconds = gb->rtc_real.seconds; + rtc_save.vba64.rtc_real.minutes = gb->rtc_real.minutes; + rtc_save.vba64.rtc_real.hours = gb->rtc_real.hours; + rtc_save.vba64.rtc_real.days = gb->rtc_real.days; + rtc_save.vba64.rtc_real.high = gb->rtc_real.high; + rtc_save.vba64.rtc_latched.seconds = gb->rtc_latched.seconds; + rtc_save.vba64.rtc_latched.minutes = gb->rtc_latched.minutes; + rtc_save.vba64.rtc_latched.hours = gb->rtc_latched.hours; + rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days; + rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high; +#ifdef GB_BIG_ENDIAN + rtc_save.vba64.last_rtc_second = __builtin_bswap64(gb->last_rtc_second); +#else + rtc_save.vba64.last_rtc_second = gb->last_rtc_second; +#endif + memcpy(buffer + gb->mbc_ram_size, &rtc_save.vba64, sizeof(rtc_save.vba64)); + } + + errno = 0; + return errno; +} + +int GB_save_battery(GB_gameboy_t *gb, const char *path) +{ + if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. + if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */ + FILE *f = fopen(path, "wb"); + if (!f) { + GB_log(gb, "Could not open battery save: %s.\n", strerror(errno)); + return errno; + } + + if (fwrite(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) { + fclose(f); + return EIO; + } + if (gb->cartridge_type->mbc_type == GB_HUC3) { +#ifdef GB_BIG_ENDIAN + GB_huc3_rtc_time_t rtc_save = { + __builtin_bswap64(gb->last_rtc_second), + __builtin_bswap16(gb->huc3_minutes), + __builtin_bswap16(gb->huc3_days), + __builtin_bswap16(gb->huc3_alarm_minutes), + __builtin_bswap16(gb->huc3_alarm_days), + gb->huc3_alarm_enabled, + }; +#else + GB_huc3_rtc_time_t rtc_save = { + gb->last_rtc_second, + gb->huc3_minutes, + gb->huc3_days, + gb->huc3_alarm_minutes, + gb->huc3_alarm_days, + gb->huc3_alarm_enabled, + }; +#endif + + if (fwrite(&rtc_save, sizeof(rtc_save), 1, f) != 1) { + fclose(f); + return EIO; + } + } + else if (gb->cartridge_type->has_rtc) { + GB_rtc_save_t rtc_save = {{{{0,}},},}; + rtc_save.vba64.rtc_real.seconds = gb->rtc_real.seconds; + rtc_save.vba64.rtc_real.minutes = gb->rtc_real.minutes; + rtc_save.vba64.rtc_real.hours = gb->rtc_real.hours; + rtc_save.vba64.rtc_real.days = gb->rtc_real.days; + rtc_save.vba64.rtc_real.high = gb->rtc_real.high; + rtc_save.vba64.rtc_latched.seconds = gb->rtc_latched.seconds; + rtc_save.vba64.rtc_latched.minutes = gb->rtc_latched.minutes; + rtc_save.vba64.rtc_latched.hours = gb->rtc_latched.hours; + rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days; + rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high; +#ifdef GB_BIG_ENDIAN + rtc_save.vba64.last_rtc_second = __builtin_bswap64(gb->last_rtc_second); +#else + rtc_save.vba64.last_rtc_second = gb->last_rtc_second; +#endif + if (fwrite(&rtc_save.vba64, 1, sizeof(rtc_save.vba64), f) != sizeof(rtc_save.vba64)) { + fclose(f); + return EIO; + } + + } + + errno = 0; + fclose(f); + return errno; +} + +void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size) +{ + memcpy(gb->mbc_ram, buffer, MIN(gb->mbc_ram_size, size)); + if (size <= gb->mbc_ram_size) { + goto reset_rtc; + } + + if (gb->cartridge_type->mbc_type == GB_HUC3) { + GB_huc3_rtc_time_t rtc_save; + if (size - gb->mbc_ram_size < sizeof(rtc_save)) { + goto reset_rtc; + } + memcpy(&rtc_save, buffer + gb->mbc_ram_size, sizeof(rtc_save)); +#ifdef GB_BIG_ENDIAN + gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); + gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); + gb->huc3_days = __builtin_bswap16(rtc_save.days); + gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); + gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; +#else + gb->last_rtc_second = rtc_save.last_rtc_second; + gb->huc3_minutes = rtc_save.minutes; + gb->huc3_days = rtc_save.days; + gb->huc3_alarm_minutes = rtc_save.alarm_minutes; + gb->huc3_alarm_days = rtc_save.alarm_days; + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; +#endif + if (gb->last_rtc_second > time(NULL)) { + /* We must reset RTC here, or it will not advance. */ + goto reset_rtc; + } + return; + } + + GB_rtc_save_t rtc_save; + memcpy(&rtc_save, buffer + gb->mbc_ram_size, MIN(sizeof(rtc_save), size)); + switch (size - gb->mbc_ram_size) { + case sizeof(rtc_save.sameboy_legacy): + memcpy(&gb->rtc_real, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); + memcpy(&gb->rtc_latched, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); + gb->last_rtc_second = rtc_save.sameboy_legacy.last_rtc_second; + break; + + case sizeof(rtc_save.vba32): + gb->rtc_real.seconds = rtc_save.vba32.rtc_real.seconds; + gb->rtc_real.minutes = rtc_save.vba32.rtc_real.minutes; + gb->rtc_real.hours = rtc_save.vba32.rtc_real.hours; + gb->rtc_real.days = rtc_save.vba32.rtc_real.days; + gb->rtc_real.high = rtc_save.vba32.rtc_real.high; + gb->rtc_latched.seconds = rtc_save.vba32.rtc_latched.seconds; + gb->rtc_latched.minutes = rtc_save.vba32.rtc_latched.minutes; + gb->rtc_latched.hours = rtc_save.vba32.rtc_latched.hours; + gb->rtc_latched.days = rtc_save.vba32.rtc_latched.days; + gb->rtc_latched.high = rtc_save.vba32.rtc_latched.high; +#ifdef GB_BIG_ENDIAN + gb->last_rtc_second = __builtin_bswap32(rtc_save.vba32.last_rtc_second); +#else + gb->last_rtc_second = rtc_save.vba32.last_rtc_second; +#endif + break; + + case sizeof(rtc_save.vba64): + gb->rtc_real.seconds = rtc_save.vba64.rtc_real.seconds; + gb->rtc_real.minutes = rtc_save.vba64.rtc_real.minutes; + gb->rtc_real.hours = rtc_save.vba64.rtc_real.hours; + gb->rtc_real.days = rtc_save.vba64.rtc_real.days; + gb->rtc_real.high = rtc_save.vba64.rtc_real.high; + gb->rtc_latched.seconds = rtc_save.vba64.rtc_latched.seconds; + gb->rtc_latched.minutes = rtc_save.vba64.rtc_latched.minutes; + gb->rtc_latched.hours = rtc_save.vba64.rtc_latched.hours; + gb->rtc_latched.days = rtc_save.vba64.rtc_latched.days; + gb->rtc_latched.high = rtc_save.vba64.rtc_latched.high; +#ifdef GB_BIG_ENDIAN + gb->last_rtc_second = __builtin_bswap64(rtc_save.vba64.last_rtc_second); +#else + gb->last_rtc_second = rtc_save.vba64.last_rtc_second; +#endif + break; + + default: + goto reset_rtc; + } + if (gb->last_rtc_second > time(NULL)) { + /* We must reset RTC here, or it will not advance. */ + goto reset_rtc; + } + + if (gb->last_rtc_second < 852076800) { /* 1/1/97. There weren't any RTC games that time, + so if the value we read is lower it means it wasn't + really RTC data. */ + goto reset_rtc; + } + goto exit; +reset_rtc: + gb->last_rtc_second = time(NULL); + gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ + gb->huc3_days = 0xFFFF; + gb->huc3_minutes = 0xFFF; + gb->huc3_alarm_enabled = false; +exit: + return; +} + +/* Loading will silently stop if the format is incomplete */ +void GB_load_battery(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + return; + } + + if (fread(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) { + goto reset_rtc; + } + + if (gb->cartridge_type->mbc_type == GB_HUC3) { + GB_huc3_rtc_time_t rtc_save; + if (fread(&rtc_save, sizeof(rtc_save), 1, f) != 1) { + goto reset_rtc; + } +#ifdef GB_BIG_ENDIAN + gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); + gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); + gb->huc3_days = __builtin_bswap16(rtc_save.days); + gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); + gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; +#else + gb->last_rtc_second = rtc_save.last_rtc_second; + gb->huc3_minutes = rtc_save.minutes; + gb->huc3_days = rtc_save.days; + gb->huc3_alarm_minutes = rtc_save.alarm_minutes; + gb->huc3_alarm_days = rtc_save.alarm_days; + gb->huc3_alarm_enabled = rtc_save.alarm_enabled; +#endif + if (gb->last_rtc_second > time(NULL)) { + /* We must reset RTC here, or it will not advance. */ + goto reset_rtc; + } + return; + } + + GB_rtc_save_t rtc_save; + switch (fread(&rtc_save, 1, sizeof(rtc_save), f)) { + case sizeof(rtc_save.sameboy_legacy): + memcpy(&gb->rtc_real, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); + memcpy(&gb->rtc_latched, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); + gb->last_rtc_second = rtc_save.sameboy_legacy.last_rtc_second; + break; + + case sizeof(rtc_save.vba32): + gb->rtc_real.seconds = rtc_save.vba32.rtc_real.seconds; + gb->rtc_real.minutes = rtc_save.vba32.rtc_real.minutes; + gb->rtc_real.hours = rtc_save.vba32.rtc_real.hours; + gb->rtc_real.days = rtc_save.vba32.rtc_real.days; + gb->rtc_real.high = rtc_save.vba32.rtc_real.high; + gb->rtc_latched.seconds = rtc_save.vba32.rtc_latched.seconds; + gb->rtc_latched.minutes = rtc_save.vba32.rtc_latched.minutes; + gb->rtc_latched.hours = rtc_save.vba32.rtc_latched.hours; + gb->rtc_latched.days = rtc_save.vba32.rtc_latched.days; + gb->rtc_latched.high = rtc_save.vba32.rtc_latched.high; +#ifdef GB_BIG_ENDIAN + gb->last_rtc_second = __builtin_bswap32(rtc_save.vba32.last_rtc_second); +#else + gb->last_rtc_second = rtc_save.vba32.last_rtc_second; +#endif + break; + + case sizeof(rtc_save.vba64): + gb->rtc_real.seconds = rtc_save.vba64.rtc_real.seconds; + gb->rtc_real.minutes = rtc_save.vba64.rtc_real.minutes; + gb->rtc_real.hours = rtc_save.vba64.rtc_real.hours; + gb->rtc_real.days = rtc_save.vba64.rtc_real.days; + gb->rtc_real.high = rtc_save.vba64.rtc_real.high; + gb->rtc_latched.seconds = rtc_save.vba64.rtc_latched.seconds; + gb->rtc_latched.minutes = rtc_save.vba64.rtc_latched.minutes; + gb->rtc_latched.hours = rtc_save.vba64.rtc_latched.hours; + gb->rtc_latched.days = rtc_save.vba64.rtc_latched.days; + gb->rtc_latched.high = rtc_save.vba64.rtc_latched.high; +#ifdef GB_BIG_ENDIAN + gb->last_rtc_second = __builtin_bswap64(rtc_save.vba64.last_rtc_second); +#else + gb->last_rtc_second = rtc_save.vba64.last_rtc_second; +#endif + break; + + default: + goto reset_rtc; + } + if (gb->last_rtc_second > time(NULL)) { + /* We must reset RTC here, or it will not advance. */ + goto reset_rtc; + } + + if (gb->last_rtc_second < 852076800) { /* 1/1/97. There weren't any RTC games that time, + so if the value we read is lower it means it wasn't + really RTC data. */ + goto reset_rtc; + } + goto exit; +reset_rtc: + gb->last_rtc_second = time(NULL); + gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ + gb->huc3_days = 0xFFFF; + gb->huc3_minutes = 0xFFF; + gb->huc3_alarm_enabled = false; +exit: + fclose(f); + return; +} + +uint8_t GB_run(GB_gameboy_t *gb) +{ + gb->vblank_just_occured = false; + + if (gb->sgb && gb->sgb->intro_animation < 140) { + /* On the SGB, the GB is halted after finishing the boot ROM. + Then, after the boot animation is almost done, it's reset. + Since the SGB HLE does not perform any header validity checks, + we just halt the CPU (with hacky code) until the correct time. + This ensures the Nintendo logo doesn't flash on screen, and + the game does "run in background" while the animation is playing. */ + GB_display_run(gb, 228); + gb->cycles_since_last_sync += 228; + return 228; + } + + GB_debugger_run(gb); + gb->cycles_since_run = 0; + GB_cpu_run(gb); + if (gb->vblank_just_occured) { + GB_rtc_run(gb); + GB_debugger_handle_async_commands(gb); + GB_rewind_push(gb); + } + return gb->cycles_since_run; +} + +uint64_t GB_run_frame(GB_gameboy_t *gb) +{ + /* Configure turbo temporarily, the user wants to handle FPS capping manually. */ + bool old_turbo = gb->turbo; + bool old_dont_skip = gb->turbo_dont_skip; + gb->turbo = true; + gb->turbo_dont_skip = true; + + gb->cycles_since_last_sync = 0; + while (true) { + GB_run(gb); + if (gb->vblank_just_occured) { + break; + } + } + gb->turbo = old_turbo; + gb->turbo_dont_skip = old_dont_skip; + return gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */ +} + +void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output) +{ + gb->screen = output; +} + +void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback) +{ + gb->vblank_callback = callback; +} + +void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback) +{ + gb->log_callback = callback; +} + +void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) +{ +#ifndef GB_DISABLE_DEBUGGER + if (gb->input_callback == default_input_callback) { + gb->async_input_callback = NULL; + } + gb->input_callback = callback; +#endif +} + +void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) +{ +#ifndef GB_DISABLE_DEBUGGER + gb->async_input_callback = callback; +#endif +} + +const GB_palette_t GB_PALETTE_GREY = {{{0x00, 0x00, 0x00}, {0x55, 0x55, 0x55}, {0xaa, 0xaa, 0xaa}, {0xff, 0xff, 0xff}, {0xff, 0xff, 0xff}}}; +const GB_palette_t GB_PALETTE_DMG = {{{0x08, 0x18, 0x10}, {0x39, 0x61, 0x39}, {0x84, 0xa5, 0x63}, {0xc6, 0xde, 0x8c}, {0xd2, 0xe6, 0xa6}}}; +const GB_palette_t GB_PALETTE_MGB = {{{0x07, 0x10, 0x0e}, {0x3a, 0x4c, 0x3a}, {0x81, 0x8d, 0x66}, {0xc2, 0xce, 0x93}, {0xcf, 0xda, 0xac}}}; +const GB_palette_t GB_PALETTE_GBL = {{{0x0a, 0x1c, 0x15}, {0x35, 0x78, 0x62}, {0x56, 0xb4, 0x95}, {0x7f, 0xe2, 0xc3}, {0x91, 0xea, 0xd0}}}; + +static void update_dmg_palette(GB_gameboy_t *gb) +{ + const GB_palette_t *palette = gb->dmg_palette ?: &GB_PALETTE_GREY; + if (gb->rgb_encode_callback && !GB_is_cgb(gb)) { + gb->sprite_palettes_rgb[4] = gb->sprite_palettes_rgb[0] = gb->background_palettes_rgb[0] = + gb->rgb_encode_callback(gb, palette->colors[3].r, palette->colors[3].g, palette->colors[3].b); + gb->sprite_palettes_rgb[5] = gb->sprite_palettes_rgb[1] = gb->background_palettes_rgb[1] = + gb->rgb_encode_callback(gb, palette->colors[2].r, palette->colors[2].g, palette->colors[2].b); + gb->sprite_palettes_rgb[6] = gb->sprite_palettes_rgb[2] = gb->background_palettes_rgb[2] = + gb->rgb_encode_callback(gb, palette->colors[1].r, palette->colors[1].g, palette->colors[1].b); + gb->sprite_palettes_rgb[7] = gb->sprite_palettes_rgb[3] = gb->background_palettes_rgb[3] = + gb->rgb_encode_callback(gb, palette->colors[0].r, palette->colors[0].g, palette->colors[0].b); + + // LCD off color + gb->background_palettes_rgb[4] = + gb->rgb_encode_callback(gb, palette->colors[4].r, palette->colors[4].g, palette->colors[4].b); + } +} + +void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette) +{ + gb->dmg_palette = palette; + update_dmg_palette(gb); +} + +void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback) +{ + + gb->rgb_encode_callback = callback; + update_dmg_palette(gb); + + for (unsigned i = 0; i < 32; i++) { + GB_palette_changed(gb, true, i * 2); + GB_palette_changed(gb, false, i * 2); + } +} + +void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback) +{ + gb->infrared_callback = callback; +} + +void GB_set_infrared_input(GB_gameboy_t *gb, bool state) +{ + gb->infrared_input = state; + gb->cycles_since_input_ir_change = 0; + gb->ir_queue_length = 0; +} + +void GB_queue_infrared_input(GB_gameboy_t *gb, bool state, uint64_t cycles_after_previous_change) +{ + if (gb->ir_queue_length == GB_MAX_IR_QUEUE) { + GB_log(gb, "IR Queue is full\n"); + return; + } + gb->ir_queue[gb->ir_queue_length++] = (GB_ir_queue_item_t){state, cycles_after_previous_change}; +} + +void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback) +{ + gb->rumble_callback = callback; +} + +void GB_set_serial_transfer_bit_start_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_start_callback_t callback) +{ + gb->serial_transfer_bit_start_callback = callback; +} + +void GB_set_serial_transfer_bit_end_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_end_callback_t callback) +{ + gb->serial_transfer_bit_end_callback = callback; +} + +bool GB_serial_get_data_bit(GB_gameboy_t *gb) +{ + if (gb->io_registers[GB_IO_SC] & 1) { + /* Internal Clock */ + GB_log(gb, "Serial read request while using internal clock. \n"); + return 0xFF; + } + return gb->io_registers[GB_IO_SB] & 0x80; +} +void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data) +{ + if (gb->io_registers[GB_IO_SC] & 1) { + /* Internal Clock */ + GB_log(gb, "Serial write request while using internal clock. \n"); + return; + } + gb->io_registers[GB_IO_SB] <<= 1; + gb->io_registers[GB_IO_SB] |= data; + gb->serial_count++; + if (gb->serial_count == 8) { + gb->io_registers[GB_IO_IF] |= 8; + gb->serial_count = 0; + } +} + +void GB_disconnect_serial(GB_gameboy_t *gb) +{ + gb->serial_transfer_bit_start_callback = NULL; + gb->serial_transfer_bit_end_callback = NULL; + + /* Reset any internally-emulated device. */ + memset(&gb->printer, 0, sizeof(gb->printer)); + memset(&gb->workboy, 0, sizeof(gb->workboy)); +} + +bool GB_is_inited(GB_gameboy_t *gb) +{ + return gb->magic == state_magic(); +} + +bool GB_is_cgb(GB_gameboy_t *gb) +{ + return (gb->model & GB_MODEL_FAMILY_MASK) == GB_MODEL_CGB_FAMILY; +} + +bool GB_is_sgb(GB_gameboy_t *gb) +{ + return (gb->model & ~GB_MODEL_PAL_BIT & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB || (gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB2; +} + +bool GB_is_hle_sgb(GB_gameboy_t *gb) +{ + return (gb->model & ~GB_MODEL_PAL_BIT) == GB_MODEL_SGB || gb->model == GB_MODEL_SGB2; +} + +void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip) +{ + gb->turbo = on; + gb->turbo_dont_skip = no_frame_skip; +} + +void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled) +{ + gb->disable_rendering = disabled; +} + +void *GB_get_user_data(GB_gameboy_t *gb) +{ + return gb->user_data; +} + +void GB_set_user_data(GB_gameboy_t *gb, void *data) +{ + gb->user_data = data; +} + +static void reset_ram(GB_gameboy_t *gb) +{ + switch (gb->model) { + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: /* Unverified */ + for (unsigned i = 0; i < gb->ram_size; i++) { + gb->ram[i] = GB_random(); + } + break; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: /* Unverified*/ + case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ + for (unsigned i = 0; i < gb->ram_size; i++) { + gb->ram[i] = GB_random(); + if (i & 0x100) { + gb->ram[i] &= GB_random(); + } + else { + gb->ram[i] |= GB_random(); + } + } + break; + + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + for (unsigned i = 0; i < gb->ram_size; i++) { + gb->ram[i] = 0x55; + gb->ram[i] ^= GB_random() & GB_random() & GB_random(); + } + break; + + case GB_MODEL_CGB_C: + for (unsigned i = 0; i < gb->ram_size; i++) { + if ((i & 0x808) == 0x800 || (i & 0x808) == 0x008) { + gb->ram[i] = 0; + } + else { + gb->ram[i] = GB_random() | GB_random() | GB_random() | GB_random(); + } + } + break; + } + + /* HRAM */ + switch (gb->model) { + case GB_MODEL_CGB_C: + // case GB_MODEL_CGB_D: + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + for (unsigned i = 0; i < sizeof(gb->hram); i++) { + gb->hram[i] = GB_random(); + } + break; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: /* Unverified*/ + case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + for (unsigned i = 0; i < sizeof(gb->hram); i++) { + if (i & 1) { + gb->hram[i] = GB_random() | GB_random() | GB_random(); + } + else { + gb->hram[i] = GB_random() & GB_random() & GB_random(); + } + } + break; + } + + /* OAM */ + switch (gb->model) { + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + /* Zero'd out by boot ROM anyway*/ + break; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: /* Unverified */ + case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + for (unsigned i = 0; i < 8; i++) { + if (i & 2) { + gb->oam[i] = GB_random() & GB_random() & GB_random(); + } + else { + gb->oam[i] = GB_random() | GB_random() | GB_random(); + } + } + for (unsigned i = 8; i < sizeof(gb->oam); i++) { + gb->oam[i] = gb->oam[i - 8]; + } + break; + } + + /* Wave RAM */ + switch (gb->model) { + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + /* Initialized by CGB-A and newer, 0s in CGB-0*/ + break; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: /* Unverified*/ + case GB_MODEL_SGB_PAL: /* Unverified */ + case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ + case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: { + uint8_t temp; + for (unsigned i = 0; i < GB_IO_WAV_END - GB_IO_WAV_START; i++) { + if (i & 1) { + temp = GB_random() & GB_random() & GB_random(); + } + else { + temp = GB_random() | GB_random() | GB_random(); + } + gb->apu.wave_channel.wave_form[i * 2] = temp >> 4; + gb->apu.wave_channel.wave_form[i * 2 + 1] = temp & 0xF; + gb->io_registers[GB_IO_WAV_START + i] = temp; + + } + break; + } + } + + for (unsigned i = 0; i < sizeof(gb->extra_oam); i++) { + gb->extra_oam[i] = GB_random(); + } + + if (GB_is_cgb(gb)) { + for (unsigned i = 0; i < 64; i++) { + gb->background_palettes_data[i] = GB_random(); /* Doesn't really matter as the boot ROM overrides it anyway*/ + gb->sprite_palettes_data[i] = GB_random(); + } + for (unsigned i = 0; i < 32; i++) { + GB_palette_changed(gb, true, i * 2); + GB_palette_changed(gb, false, i * 2); + } + } +} + +static void request_boot_rom(GB_gameboy_t *gb) +{ + if (gb->boot_rom_load_callback) { + GB_boot_rom_t type = 0; + switch (gb->model) { + case GB_MODEL_DMG_B: + type = GB_BOOT_ROM_DMG; + break; + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: + type = GB_BOOT_ROM_SGB; + break; + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + type = GB_BOOT_ROM_SGB2; + break; + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_E: + type = GB_BOOT_ROM_CGB; + break; + case GB_MODEL_AGB: + type = GB_BOOT_ROM_AGB; + break; + } + gb->boot_rom_load_callback(gb, type); + } +} + +void GB_reset(GB_gameboy_t *gb) +{ + uint32_t mbc_ram_size = gb->mbc_ram_size; + GB_model_t model = gb->model; + memset(gb, 0, (size_t)GB_GET_SECTION((GB_gameboy_t *) 0, unsaved)); + gb->model = model; + gb->version = GB_STRUCT_VERSION; + + gb->mbc_rom_bank = 1; + gb->last_rtc_second = time(NULL); + gb->cgb_ram_bank = 1; + gb->io_registers[GB_IO_JOYP] = 0xCF; + gb->mbc_ram_size = mbc_ram_size; + if (GB_is_cgb(gb)) { + gb->ram_size = 0x1000 * 8; + gb->vram_size = 0x2000 * 2; + memset(gb->vram, 0, gb->vram_size); + gb->cgb_mode = true; + gb->object_priority = GB_OBJECT_PRIORITY_INDEX; + } + else { + gb->ram_size = 0x2000; + gb->vram_size = 0x2000; + memset(gb->vram, 0, gb->vram_size); + gb->object_priority = GB_OBJECT_PRIORITY_X; + + update_dmg_palette(gb); + } + reset_ram(gb); + + /* The serial interrupt always occur on the 0xF7th cycle of every 0x100 cycle since boot. */ + gb->serial_cycles = 0x100-0xF7; + gb->io_registers[GB_IO_SC] = 0x7E; + + /* These are not deterministic, but 00 (CGB) and FF (DMG) are the most common initial values by far */ + gb->io_registers[GB_IO_DMA] = gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = GB_is_cgb(gb)? 0x00 : 0xFF; + + gb->accessed_oam_row = -1; + + + if (GB_is_hle_sgb(gb)) { + if (!gb->sgb) { + gb->sgb = malloc(sizeof(*gb->sgb)); + } + memset(gb->sgb, 0, sizeof(*gb->sgb)); + memset(gb->sgb_intro_jingle_phases, 0, sizeof(gb->sgb_intro_jingle_phases)); + gb->sgb_intro_sweep_phase = 0; + gb->sgb_intro_sweep_previous_sample = 0; + gb->sgb->intro_animation = -10; + + gb->sgb->player_count = 1; + GB_sgb_load_default_data(gb); + + } + else { + if (gb->sgb) { + free(gb->sgb); + gb->sgb = NULL; + } + } + + /* Todo: Ugly, fixme, see comment in the timer state machine */ + gb->div_state = 3; + + GB_apu_update_cycles_per_sample(gb); + + if (gb->nontrivial_jump_state) { + free(gb->nontrivial_jump_state); + gb->nontrivial_jump_state = NULL; + } + + gb->magic = state_magic(); + request_boot_rom(gb); +} + +void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model) +{ + gb->model = model; + if (GB_is_cgb(gb)) { + gb->ram = realloc(gb->ram, gb->ram_size = 0x1000 * 8); + gb->vram = realloc(gb->vram, gb->vram_size = 0x2000 * 2); + } + else { + gb->ram = realloc(gb->ram, gb->ram_size = 0x2000); + gb->vram = realloc(gb->vram, gb->vram_size = 0x2000); + } + GB_rewind_free(gb); + GB_reset(gb); + load_default_border(gb); +} + +void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *size, uint16_t *bank) +{ + /* Set size and bank to dummy pointers if not set */ + size_t dummy_size; + uint16_t dummy_bank; + if (!size) { + size = &dummy_size; + } + + if (!bank) { + bank = &dummy_bank; + } + + + switch (access) { + case GB_DIRECT_ACCESS_ROM: + *size = gb->rom_size; + *bank = gb->mbc_rom_bank; + return gb->rom; + case GB_DIRECT_ACCESS_RAM: + *size = gb->ram_size; + *bank = gb->cgb_ram_bank; + return gb->ram; + case GB_DIRECT_ACCESS_CART_RAM: + *size = gb->mbc_ram_size; + *bank = gb->mbc_ram_bank; + return gb->mbc_ram; + case GB_DIRECT_ACCESS_VRAM: + *size = gb->vram_size; + *bank = gb->cgb_vram_bank; + return gb->vram; + case GB_DIRECT_ACCESS_HRAM: + *size = sizeof(gb->hram); + *bank = 0; + return &gb->hram; + case GB_DIRECT_ACCESS_IO: + *size = sizeof(gb->io_registers); + *bank = 0; + return &gb->io_registers; + case GB_DIRECT_ACCESS_BOOTROM: + *size = GB_is_cgb(gb)? sizeof(gb->boot_rom) : 0x100; + *bank = 0; + return &gb->boot_rom; + case GB_DIRECT_ACCESS_OAM: + *size = sizeof(gb->oam); + *bank = 0; + return &gb->oam; + case GB_DIRECT_ACCESS_BGP: + *size = sizeof(gb->background_palettes_data); + *bank = 0; + return &gb->background_palettes_data; + case GB_DIRECT_ACCESS_OBP: + *size = sizeof(gb->sprite_palettes_data); + *bank = 0; + return &gb->sprite_palettes_data; + case GB_DIRECT_ACCESS_IE: + *size = sizeof(gb->interrupt_enable); + *bank = 0; + return &gb->interrupt_enable; + default: + *size = 0; + *bank = 0; + return NULL; + } +} + +void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier) +{ + gb->clock_multiplier = multiplier; + GB_apu_update_cycles_per_sample(gb); +} + +uint32_t GB_get_clock_rate(GB_gameboy_t *gb) +{ + if (gb->model & GB_MODEL_PAL_BIT) { + return SGB_PAL_FREQUENCY * gb->clock_multiplier; + } + if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) { + return SGB_NTSC_FREQUENCY * gb->clock_multiplier; + } + return CPU_FREQUENCY * gb->clock_multiplier; +} + +void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode) +{ + if (gb->border_mode > GB_BORDER_ALWAYS) return; + gb->border_mode = border_mode; +} + +unsigned GB_get_screen_width(GB_gameboy_t *gb) +{ + switch (gb->border_mode) { + default: + case GB_BORDER_SGB: + return GB_is_hle_sgb(gb)? 256 : 160; + case GB_BORDER_NEVER: + return 160; + case GB_BORDER_ALWAYS: + return 256; + } +} + +unsigned GB_get_screen_height(GB_gameboy_t *gb) +{ + switch (gb->border_mode) { + default: + case GB_BORDER_SGB: + return GB_is_hle_sgb(gb)? 224 : 144; + case GB_BORDER_NEVER: + return 144; + case GB_BORDER_ALWAYS: + return 224; + } +} + +unsigned GB_get_player_count(GB_gameboy_t *gb) +{ + return GB_is_hle_sgb(gb)? gb->sgb->player_count : 1; +} + +void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback) +{ + gb->update_input_hint_callback = callback; +} + +double GB_get_usual_frame_rate(GB_gameboy_t *gb) +{ + return GB_get_clock_rate(gb) / (double)LCDC_PERIOD; +} + +void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback) +{ + gb->joyp_write_callback = callback; +} + +void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback) +{ + gb->icd_pixel_callback = callback; +} + +void GB_set_icd_hreset_callback(GB_gameboy_t *gb, GB_icd_hreset_callback_t callback) +{ + gb->icd_hreset_callback = callback; +} + + +void GB_set_icd_vreset_callback(GB_gameboy_t *gb, GB_icd_vreset_callback_t callback) +{ + gb->icd_vreset_callback = callback; +} + +void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t callback) +{ + gb->boot_rom_load_callback = callback; + request_boot_rom(gb); +} + +unsigned GB_time_to_alarm(GB_gameboy_t *gb) +{ + if (gb->cartridge_type->mbc_type != GB_HUC3) return 0; + if (!gb->huc3_alarm_enabled) return 0; + if (!(gb->huc3_alarm_days & 0x2000)) return 0; + unsigned current_time = (gb->huc3_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_minutes * 60 + (time(NULL) % 60); + unsigned alarm_time = (gb->huc3_alarm_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_alarm_minutes * 60; + if (current_time > alarm_time) return 0; + return alarm_time - current_time; +} diff --git a/bsnes/gb/Core/gb.h b/bsnes/gb/Core/gb.h new file mode 100644 index 00000000..90439368 --- /dev/null +++ b/bsnes/gb/Core/gb.h @@ -0,0 +1,816 @@ +#ifndef GB_h +#define GB_h +#define typeof __typeof__ +#include +#include +#include + +#include "gb_struct_def.h" +#include "save_state.h" + +#include "apu.h" +#include "camera.h" +#include "debugger.h" +#include "display.h" +#include "joypad.h" +#include "mbc.h" +#include "memory.h" +#include "printer.h" +#include "timing.h" +#include "rewind.h" +#include "sm83_cpu.h" +#include "symbol_hash.h" +#include "sgb.h" +#include "cheats.h" +#include "rumble.h" +#include "workboy.h" + +#define GB_STRUCT_VERSION 13 + +#define GB_MODEL_FAMILY_MASK 0xF00 +#define GB_MODEL_DMG_FAMILY 0x000 +#define GB_MODEL_MGB_FAMILY 0x100 +#define GB_MODEL_CGB_FAMILY 0x200 +#define GB_MODEL_PAL_BIT 0x1000 +#define GB_MODEL_NO_SFC_BIT 0x2000 + +#ifdef GB_INTERNAL +#if __clang__ +#define UNROLL _Pragma("unroll") +#elif __GNUC__ >= 8 +#define UNROLL _Pragma("GCC unroll 8") +#else +#define UNROLL +#endif + +#endif + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define GB_BIG_ENDIAN +#elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define GB_LITTLE_ENDIAN +#else +#error Unable to detect endianess +#endif + +#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8) +#define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; }) +#endif + +typedef struct { + struct { + uint8_t r, g, b; + } colors[5]; +} GB_palette_t; + +extern const GB_palette_t GB_PALETTE_GREY; +extern const GB_palette_t GB_PALETTE_DMG; +extern const GB_palette_t GB_PALETTE_MGB; +extern const GB_palette_t GB_PALETTE_GBL; + +typedef union { + struct { + uint8_t seconds; + uint8_t minutes; + uint8_t hours; + uint8_t days; + uint8_t high; + }; + uint8_t data[5]; +} GB_rtc_time_t; + +typedef enum { + // GB_MODEL_DMG_0 = 0x000, + // GB_MODEL_DMG_A = 0x001, + GB_MODEL_DMG_B = 0x002, + // GB_MODEL_DMG_C = 0x003, + GB_MODEL_SGB = 0x004, + GB_MODEL_SGB_NTSC = GB_MODEL_SGB, + GB_MODEL_SGB_PAL = GB_MODEL_SGB | GB_MODEL_PAL_BIT, + GB_MODEL_SGB_NTSC_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT, + GB_MODEL_SGB_NO_SFC = GB_MODEL_SGB_NTSC_NO_SFC, + GB_MODEL_SGB_PAL_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT | GB_MODEL_PAL_BIT, + // GB_MODEL_MGB = 0x100, + GB_MODEL_SGB2 = 0x101, + GB_MODEL_SGB2_NO_SFC = GB_MODEL_SGB2 | GB_MODEL_NO_SFC_BIT, + // GB_MODEL_CGB_0 = 0x200, + // GB_MODEL_CGB_A = 0x201, + // GB_MODEL_CGB_B = 0x202, + GB_MODEL_CGB_C = 0x203, + // GB_MODEL_CGB_D = 0x204, + GB_MODEL_CGB_E = 0x205, + GB_MODEL_AGB = 0x206, +} GB_model_t; + +enum { + GB_REGISTER_AF, + GB_REGISTER_BC, + GB_REGISTER_DE, + GB_REGISTER_HL, + GB_REGISTER_SP, + GB_REGISTERS_16_BIT /* Count */ +}; + +/* Todo: Actually use these! */ +enum { + GB_CARRY_FLAG = 16, + GB_HALF_CARRY_FLAG = 32, + GB_SUBTRACT_FLAG = 64, + GB_ZERO_FLAG = 128, +}; + +typedef enum { + GB_BORDER_SGB, + GB_BORDER_NEVER, + GB_BORDER_ALWAYS, +} GB_border_mode_t; + +#define GB_MAX_IR_QUEUE 256 + +enum { + /* Joypad and Serial */ + GB_IO_JOYP = 0x00, // Joypad (R/W) + GB_IO_SB = 0x01, // Serial transfer data (R/W) + GB_IO_SC = 0x02, // Serial Transfer Control (R/W) + + /* Missing */ + + /* Timers */ + GB_IO_DIV = 0x04, // Divider Register (R/W) + GB_IO_TIMA = 0x05, // Timer counter (R/W) + GB_IO_TMA = 0x06, // Timer Modulo (R/W) + GB_IO_TAC = 0x07, // Timer Control (R/W) + + /* Missing */ + + GB_IO_IF = 0x0f, // Interrupt Flag (R/W) + + /* Sound */ + GB_IO_NR10 = 0x10, // Channel 1 Sweep register (R/W) + GB_IO_NR11 = 0x11, // Channel 1 Sound length/Wave pattern duty (R/W) + GB_IO_NR12 = 0x12, // Channel 1 Volume Envelope (R/W) + GB_IO_NR13 = 0x13, // Channel 1 Frequency lo (Write Only) + GB_IO_NR14 = 0x14, // Channel 1 Frequency hi (R/W) + /* NR20 does not exist */ + GB_IO_NR21 = 0x16, // Channel 2 Sound Length/Wave Pattern Duty (R/W) + GB_IO_NR22 = 0x17, // Channel 2 Volume Envelope (R/W) + GB_IO_NR23 = 0x18, // Channel 2 Frequency lo data (W) + GB_IO_NR24 = 0x19, // Channel 2 Frequency hi data (R/W) + GB_IO_NR30 = 0x1a, // Channel 3 Sound on/off (R/W) + GB_IO_NR31 = 0x1b, // Channel 3 Sound Length + GB_IO_NR32 = 0x1c, // Channel 3 Select output level (R/W) + GB_IO_NR33 = 0x1d, // Channel 3 Frequency's lower data (W) + GB_IO_NR34 = 0x1e, // Channel 3 Frequency's higher data (R/W) + /* NR40 does not exist */ + GB_IO_NR41 = 0x20, // Channel 4 Sound Length (R/W) + GB_IO_NR42 = 0x21, // Channel 4 Volume Envelope (R/W) + GB_IO_NR43 = 0x22, // Channel 4 Polynomial Counter (R/W) + GB_IO_NR44 = 0x23, // Channel 4 Counter/consecutive, Inital (R/W) + GB_IO_NR50 = 0x24, // Channel control / ON-OFF / Volume (R/W) + GB_IO_NR51 = 0x25, // Selection of Sound output terminal (R/W) + GB_IO_NR52 = 0x26, // Sound on/off + + /* Missing */ + + GB_IO_WAV_START = 0x30, // Wave pattern start + GB_IO_WAV_END = 0x3f, // Wave pattern end + + /* Graphics */ + GB_IO_LCDC = 0x40, // LCD Control (R/W) + GB_IO_STAT = 0x41, // LCDC Status (R/W) + GB_IO_SCY = 0x42, // Scroll Y (R/W) + GB_IO_SCX = 0x43, // Scroll X (R/W) + GB_IO_LY = 0x44, // LCDC Y-Coordinate (R) + GB_IO_LYC = 0x45, // LY Compare (R/W) + GB_IO_DMA = 0x46, // DMA Transfer and Start Address (W) + GB_IO_BGP = 0x47, // BG Palette Data (R/W) - Non CGB Mode Only + GB_IO_OBP0 = 0x48, // Object Palette 0 Data (R/W) - Non CGB Mode Only + GB_IO_OBP1 = 0x49, // Object Palette 1 Data (R/W) - Non CGB Mode Only + GB_IO_WY = 0x4a, // Window Y Position (R/W) + GB_IO_WX = 0x4b, // Window X Position minus 7 (R/W) + // Has some undocumented compatibility flags written at boot. + // Unfortunately it is not readable or writable after boot has finished, so research of this + // register is quite limited. The value written to this register, however, can be controlled + // in some cases. + GB_IO_KEY0 = 0x4c, + + /* General CGB features */ + GB_IO_KEY1 = 0x4d, // CGB Mode Only - Prepare Speed Switch + + /* Missing */ + + GB_IO_VBK = 0x4f, // CGB Mode Only - VRAM Bank + GB_IO_BANK = 0x50, // Write to disable the BIOS mapping + + /* CGB DMA */ + GB_IO_HDMA1 = 0x51, // CGB Mode Only - New DMA Source, High + GB_IO_HDMA2 = 0x52, // CGB Mode Only - New DMA Source, Low + GB_IO_HDMA3 = 0x53, // CGB Mode Only - New DMA Destination, High + GB_IO_HDMA4 = 0x54, // CGB Mode Only - New DMA Destination, Low + GB_IO_HDMA5 = 0x55, // CGB Mode Only - New DMA Length/Mode/Start + + /* IR */ + GB_IO_RP = 0x56, // CGB Mode Only - Infrared Communications Port + + /* Missing */ + + /* CGB Paletts */ + GB_IO_BGPI = 0x68, // CGB Mode Only - Background Palette Index + GB_IO_BGPD = 0x69, // CGB Mode Only - Background Palette Data + GB_IO_OBPI = 0x6a, // CGB Mode Only - Sprite Palette Index + GB_IO_OBPD = 0x6b, // CGB Mode Only - Sprite Palette Data + GB_IO_OPRI = 0x6c, // Affects object priority (X based or index based) + + /* Missing */ + + GB_IO_SVBK = 0x70, // CGB Mode Only - WRAM Bank + GB_IO_UNKNOWN2 = 0x72, // (00h) - Bit 0-7 (Read/Write) + GB_IO_UNKNOWN3 = 0x73, // (00h) - Bit 0-7 (Read/Write) + GB_IO_UNKNOWN4 = 0x74, // (00h) - Bit 0-7 (Read/Write) - CGB Mode Only + GB_IO_UNKNOWN5 = 0x75, // (8Fh) - Bit 4-6 (Read/Write) + GB_IO_PCM_12 = 0x76, // Channels 1 and 2 amplitudes + GB_IO_PCM_34 = 0x77, // Channels 3 and 4 amplitudes + GB_IO_UNKNOWN8 = 0x7F, // Unknown, write only +}; + +typedef enum { + GB_LOG_BOLD = 1, + GB_LOG_DASHED_UNDERLINE = 2, + GB_LOG_UNDERLINE = 4, + GB_LOG_UNDERLINE_MASK = GB_LOG_DASHED_UNDERLINE | GB_LOG_UNDERLINE +} GB_log_attributes; + +typedef enum { + GB_BOOT_ROM_DMG0, + GB_BOOT_ROM_DMG, + GB_BOOT_ROM_MGB, + GB_BOOT_ROM_SGB, + GB_BOOT_ROM_SGB2, + GB_BOOT_ROM_CGB0, + GB_BOOT_ROM_CGB, + GB_BOOT_ROM_AGB, +} GB_boot_rom_t; + +#ifdef GB_INTERNAL +#define LCDC_PERIOD 70224 +#define CPU_FREQUENCY 0x400000 +#define SGB_NTSC_FREQUENCY (21477272 / 5) +#define SGB_PAL_FREQUENCY (21281370 / 5) +#define DIV_CYCLES (0x100) +#define INTERNAL_DIV_CYCLES (0x40000) + +#if !defined(MIN) +#define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#endif + +#if !defined(MAX) +#define MAX(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) +#endif +#endif + +typedef void (*GB_vblank_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes); +typedef char *(*GB_input_callback_t)(GB_gameboy_t *gb); +typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b); +typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on, uint64_t cycles_since_last_update); +typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, double rumble_amplitude); +typedef void (*GB_serial_transfer_bit_start_callback_t)(GB_gameboy_t *gb, bool bit_to_send); +typedef bool (*GB_serial_transfer_bit_end_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_update_input_hint_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_joyp_write_callback_t)(GB_gameboy_t *gb, uint8_t value); +typedef void (*GB_icd_pixel_callback_t)(GB_gameboy_t *gb, uint8_t row); +typedef void (*GB_icd_hreset_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_icd_vreset_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_boot_rom_load_callback_t)(GB_gameboy_t *gb, GB_boot_rom_t type); + +typedef struct { + bool state; + uint64_t delay; +} GB_ir_queue_item_t; + +struct GB_breakpoint_s; +struct GB_watchpoint_s; + +typedef struct { + uint8_t pixel; // Color, 0-3 + uint8_t palette; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) + uint8_t priority; // Sprite priority – 0 in DMG, OAM index in CGB + bool bg_priority; // For sprite FIFO – the BG priority bit. For the BG FIFO – the CGB attributes priority bit +} GB_fifo_item_t; + +#define GB_FIFO_LENGTH 16 +typedef struct { + GB_fifo_item_t fifo[GB_FIFO_LENGTH]; + uint8_t read_end; + uint8_t write_end; +} GB_fifo_t; + +/* When state saving, each section is dumped independently of other sections. + This allows adding data to the end of the section without worrying about future compatibility. + Some other changes might be "safe" as well. + This struct is not packed, but dumped sections exclusively use types that have the same alignment in both 32 and 64 + bit platforms. */ + +#ifdef GB_INTERNAL +struct GB_gameboy_s { +#else +struct GB_gameboy_internal_s { +#endif + GB_SECTION(header, + /* The magic makes sure a state file is: + - Indeed a SameBoy state file. + - Has the same endianess has the current platform. */ + volatile uint32_t magic; + /* The version field makes sure we don't load save state files with a completely different structure. + This happens when struct fields are removed/resized in an backward incompatible manner. */ + uint32_t version; + ); + + GB_SECTION(core_state, + /* Registers */ + uint16_t pc; + union { + uint16_t registers[GB_REGISTERS_16_BIT]; + struct { + uint16_t af, + bc, + de, + hl, + sp; + }; + struct { +#ifdef GB_BIG_ENDIAN + uint8_t a, f, + b, c, + d, e, + h, l; +#else + uint8_t f, a, + c, b, + e, d, + l, h; +#endif + }; + + }; + uint8_t ime; + uint8_t interrupt_enable; + uint8_t cgb_ram_bank; + + /* CPU and General Hardware Flags*/ + GB_model_t model; + bool cgb_mode; + bool cgb_double_speed; + bool halted; + bool stopped; + bool boot_rom_finished; + bool ime_toggle; /* ei has delayed a effect.*/ + bool halt_bug; + bool just_halted; + + /* Misc state */ + bool infrared_input; + GB_printer_t printer; + uint8_t extra_oam[0xff00 - 0xfea0]; + uint32_t ram_size; // Different between CGB and DMG + GB_workboy_t workboy; + ); + + /* DMA and HDMA */ + GB_SECTION(dma, + bool hdma_on; + bool hdma_on_hblank; + uint8_t hdma_steps_left; + int16_t hdma_cycles; // in 8MHz units + uint16_t hdma_current_src, hdma_current_dest; + + uint8_t dma_steps_left; + uint8_t dma_current_dest; + uint16_t dma_current_src; + int16_t dma_cycles; + bool is_dma_restarting; + uint8_t last_opcode_read; /* Required to emulte HDMA reads from Exxx */ + bool hdma_starting; + ); + + /* MBC */ + GB_SECTION(mbc, + uint16_t mbc_rom_bank; + uint8_t mbc_ram_bank; + uint32_t mbc_ram_size; + bool mbc_ram_enable; + union { + struct { + uint8_t bank_low:5; + uint8_t bank_high:2; + uint8_t mode:1; + } mbc1; + + struct { + uint8_t rom_bank:4; + } mbc2; + + struct { + uint8_t rom_bank:8; + uint8_t ram_bank:3; + } mbc3; + + struct { + uint8_t rom_bank_low; + uint8_t rom_bank_high:1; + uint8_t ram_bank:4; + } mbc5; + + struct { + uint8_t bank_low:6; + uint8_t bank_high:3; + bool mode:1; + bool ir_mode:1; + } huc1; + + struct { + uint8_t rom_bank:7; + uint8_t padding:1; + uint8_t ram_bank:4; + } huc3; + }; + uint16_t mbc_rom0_bank; /* For some MBC1 wirings. */ + bool camera_registers_mapped; + uint8_t camera_registers[0x36]; + bool rumble_state; + bool cart_ir; + + // TODO: move to huc3/mbc3 struct when breaking save compat + uint8_t huc3_mode; + uint8_t huc3_access_index; + uint16_t huc3_minutes, huc3_days; + uint16_t huc3_alarm_minutes, huc3_alarm_days; + bool huc3_alarm_enabled; + uint8_t huc3_read; + uint8_t huc3_access_flags; + bool mbc3_rtc_mapped; + ); + + + /* HRAM and HW Registers */ + GB_SECTION(hram, + uint8_t hram[0xFFFF - 0xFF80]; + uint8_t io_registers[0x80]; + ); + + /* Timing */ + GB_SECTION(timing, + GB_UNIT(display); + GB_UNIT(div); + uint16_t div_counter; + uint8_t tima_reload_state; /* After TIMA overflows, it becomes 0 for 4 cycles before actually reloading. */ + uint16_t serial_cycles; + uint16_t serial_length; + uint8_t double_speed_alignment; + uint8_t serial_count; + ); + + /* APU */ + GB_SECTION(apu, + GB_apu_t apu; + ); + + /* RTC */ + GB_SECTION(rtc, + GB_rtc_time_t rtc_real, rtc_latched; + uint64_t last_rtc_second; + bool rtc_latch; + ); + + /* Video Display */ + GB_SECTION(video, + uint32_t vram_size; // Different between CGB and DMG + uint8_t cgb_vram_bank; + uint8_t oam[0xA0]; + uint8_t background_palettes_data[0x40]; + uint8_t sprite_palettes_data[0x40]; + uint8_t position_in_line; + bool stat_interrupt_line; + uint8_t effective_scx; + uint8_t window_y; + /* The LCDC will skip the first frame it renders after turning it on. + On the CGB, a frame is not skipped if the previous frame was skipped as well. + See https://www.reddit.com/r/EmuDev/comments/6exyxu/ */ + + /* TODO: Drop this and properly emulate the dropped vreset signal*/ + enum { + GB_FRAMESKIP_LCD_TURNED_ON, // On a DMG, the LCD renders a blank screen during this state, + // on a CGB, the previous frame is repeated (which might be + // blank if the LCD was off for more than a few cycles) + GB_FRAMESKIP_FIRST_FRAME_SKIPPED, // This state is 'skipped' when emulating a DMG + GB_FRAMESKIP_SECOND_FRAME_RENDERED, + } frame_skip_state; + bool oam_read_blocked; + bool vram_read_blocked; + bool oam_write_blocked; + bool vram_write_blocked; + bool fifo_insertion_glitch; + uint8_t current_line; + uint16_t ly_for_comparison; + GB_fifo_t bg_fifo, oam_fifo; + uint8_t fetcher_x; + uint8_t fetcher_y; + uint16_t cycles_for_line; + uint8_t current_tile; + uint8_t current_tile_attributes; + uint8_t current_tile_data[2]; + uint8_t fetcher_state; + bool window_is_being_fetched; + bool wx166_glitch; + bool wx_triggered; + uint8_t visible_objs[10]; + uint8_t obj_comparators[10]; + uint8_t n_visible_objs; + uint8_t oam_search_index; + uint8_t accessed_oam_row; + uint8_t extra_penalty_for_sprite_at_0; + uint8_t mode_for_interrupt; + bool lyc_interrupt_line; + bool cgb_palettes_blocked; + uint8_t current_lcd_line; // The LCD can go out of sync since the vsync signal is skipped in some cases. + uint32_t cycles_in_stop_mode; + uint8_t object_priority; + bool oam_ppu_blocked; + bool vram_ppu_blocked; + bool cgb_palettes_ppu_blocked; + bool object_fetch_aborted; + bool during_object_fetch; + uint16_t object_low_line_address; + bool wy_triggered; + uint8_t window_tile_x; + uint8_t lcd_x; // The LCD can go out of sync since the push signal is skipped in some cases. + bool is_odd_frame; + uint16_t last_tile_data_address; + uint16_t last_tile_index_address; + bool cgb_repeated_a_frame; + ); + + /* Unsaved data. This includes all pointers, as well as everything that shouldn't be on a save state */ + /* This data is reserved on reset and must come last in the struct */ + GB_SECTION(unsaved, + /* ROM */ + uint8_t *rom; + uint32_t rom_size; + const GB_cartridge_t *cartridge_type; + enum { + GB_STANDARD_MBC1_WIRING, + GB_MBC1M_WIRING, + } mbc1_wiring; + bool is_mbc30; + + unsigned pending_cycles; + + /* Various RAMs */ + uint8_t *ram; + uint8_t *vram; + uint8_t *mbc_ram; + + /* I/O */ + uint32_t *screen; + uint32_t background_palettes_rgb[0x20]; + uint32_t sprite_palettes_rgb[0x20]; + const GB_palette_t *dmg_palette; + GB_color_correction_mode_t color_correction_mode; + bool keys[4][GB_KEY_MAX]; + GB_border_mode_t border_mode; + GB_sgb_border_t borrowed_border; + bool tried_loading_sgb_border; + bool has_sgb_border; + + /* Timing */ + uint64_t last_sync; + uint64_t cycles_since_last_sync; // In 8MHz units + + /* Audio */ + GB_apu_output_t apu_output; + + /* Callbacks */ + void *user_data; + GB_log_callback_t log_callback; + GB_input_callback_t input_callback; + GB_input_callback_t async_input_callback; + GB_rgb_encode_callback_t rgb_encode_callback; + GB_vblank_callback_t vblank_callback; + GB_infrared_callback_t infrared_callback; + GB_camera_get_pixel_callback_t camera_get_pixel_callback; + GB_camera_update_request_callback_t camera_update_request_callback; + GB_rumble_callback_t rumble_callback; + GB_serial_transfer_bit_start_callback_t serial_transfer_bit_start_callback; + GB_serial_transfer_bit_end_callback_t serial_transfer_bit_end_callback; + GB_update_input_hint_callback_t update_input_hint_callback; + GB_joyp_write_callback_t joyp_write_callback; + GB_icd_pixel_callback_t icd_pixel_callback; + GB_icd_vreset_callback_t icd_hreset_callback; + GB_icd_vreset_callback_t icd_vreset_callback; + GB_read_memory_callback_t read_memory_callback; + GB_boot_rom_load_callback_t boot_rom_load_callback; + GB_print_image_callback_t printer_callback; + GB_workboy_set_time_callback workboy_set_time_callback; + GB_workboy_get_time_callback workboy_get_time_callback; + + /* IR */ + uint64_t cycles_since_ir_change; // In 8MHz units + uint64_t cycles_since_input_ir_change; // In 8MHz units + GB_ir_queue_item_t ir_queue[GB_MAX_IR_QUEUE]; + size_t ir_queue_length; + + /*** Debugger ***/ + volatile bool debug_stopped, debug_disable; + bool debug_fin_command, debug_next_command; + + /* Breakpoints */ + uint16_t n_breakpoints; + struct GB_breakpoint_s *breakpoints; + bool has_jump_to_breakpoints, has_software_breakpoints; + void *nontrivial_jump_state; + bool non_trivial_jump_breakpoint_occured; + + /* SLD (Todo: merge with backtrace) */ + bool stack_leak_detection; + signed debug_call_depth; + uint16_t sp_for_call_depth[0x200]; /* Should be much more than enough */ + uint16_t addr_for_call_depth[0x200]; + + /* Backtrace */ + unsigned backtrace_size; + uint16_t backtrace_sps[0x200]; + struct { + uint16_t bank; + uint16_t addr; + } backtrace_returns[0x200]; + + /* Watchpoints */ + uint16_t n_watchpoints; + struct GB_watchpoint_s *watchpoints; + + /* Symbol tables */ + GB_symbol_map_t *bank_symbols[0x200]; + GB_reversed_symbol_map_t reversed_symbol_map; + + /* Ticks command */ + uint64_t debugger_ticks; + + /* Rewind */ +#define GB_REWIND_FRAMES_PER_KEY 255 + size_t rewind_buffer_length; + struct { + uint8_t *key_state; + uint8_t *compressed_states[GB_REWIND_FRAMES_PER_KEY]; + unsigned pos; + } *rewind_sequences; // lasts about 4 seconds + size_t rewind_pos; + + /* SGB - saved and allocated optionally */ + GB_sgb_t *sgb; + + double sgb_intro_jingle_phases[7]; + double sgb_intro_sweep_phase; + double sgb_intro_sweep_previous_sample; + + /* Cheats */ + bool cheat_enabled; + size_t cheat_count; + GB_cheat_t **cheats; + GB_cheat_hash_t *cheat_hash[256]; + + /* Misc */ + bool turbo; + bool turbo_dont_skip; + bool disable_rendering; + uint8_t boot_rom[0x900]; + bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank + uint8_t cycles_since_run; // How many cycles have passed since the last call to GB_run(), in 8MHz units + double clock_multiplier; + GB_rumble_mode_t rumble_mode; + uint32_t rumble_on_cycles; + uint32_t rumble_off_cycles; + + /* Temporary state */ + bool wx_just_changed; + ); +}; + +#ifndef GB_INTERNAL +struct GB_gameboy_s { + char __internal[sizeof(struct GB_gameboy_internal_s)]; +}; +#endif + + +#ifndef __printflike +/* Missing from Linux headers. */ +#define __printflike(fmtarg, firstvararg) \ +__attribute__((__format__ (__printf__, fmtarg, firstvararg))) +#endif + +void GB_init(GB_gameboy_t *gb, GB_model_t model); +bool GB_is_inited(GB_gameboy_t *gb); +bool GB_is_cgb(GB_gameboy_t *gb); +bool GB_is_sgb(GB_gameboy_t *gb); // Returns true if the model is SGB or SGB2 +bool GB_is_hle_sgb(GB_gameboy_t *gb); // Returns true if the model is SGB or SGB2 and the SFC/SNES side is HLE'd +GB_model_t GB_get_model(GB_gameboy_t *gb); +void GB_free(GB_gameboy_t *gb); +void GB_reset(GB_gameboy_t *gb); +void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model); + +/* Returns the time passed, in 8MHz ticks. */ +uint8_t GB_run(GB_gameboy_t *gb); +/* Returns the time passed since the last frame, in nanoseconds */ +uint64_t GB_run_frame(GB_gameboy_t *gb); + +typedef enum { + GB_DIRECT_ACCESS_ROM, + GB_DIRECT_ACCESS_RAM, + GB_DIRECT_ACCESS_CART_RAM, + GB_DIRECT_ACCESS_VRAM, + GB_DIRECT_ACCESS_HRAM, + GB_DIRECT_ACCESS_IO, /* Warning: Some registers can only be read/written correctly via GB_memory_read/write. */ + GB_DIRECT_ACCESS_BOOTROM, + GB_DIRECT_ACCESS_OAM, + GB_DIRECT_ACCESS_BGP, + GB_DIRECT_ACCESS_OBP, + GB_DIRECT_ACCESS_IE, +} GB_direct_access_t; + +/* Returns a mutable pointer to various hardware memories. If that memory is banked, the current bank + is returned at *bank, even if only a portion of the memory is banked. */ +void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *size, uint16_t *bank); + +void *GB_get_user_data(GB_gameboy_t *gb); +void GB_set_user_data(GB_gameboy_t *gb, void *data); + + + +int GB_load_boot_rom(GB_gameboy_t *gb, const char *path); +void GB_load_boot_rom_from_buffer(GB_gameboy_t *gb, const unsigned char *buffer, size_t size); +int GB_load_rom(GB_gameboy_t *gb, const char *path); +void GB_load_rom_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size); +int GB_load_isx(GB_gameboy_t *gb, const char *path); + +int GB_save_battery_size(GB_gameboy_t *gb); +int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size); +int GB_save_battery(GB_gameboy_t *gb, const char *path); + +void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size); +void GB_load_battery(GB_gameboy_t *gb, const char *path); + +void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip); +void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled); + +void GB_log(GB_gameboy_t *gb, const char *fmt, ...) __printflike(2, 3); +void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...) __printflike(3, 4); + +void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output); +void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode); + +void GB_set_infrared_input(GB_gameboy_t *gb, bool state); +void GB_queue_infrared_input(GB_gameboy_t *gb, bool state, uint64_t cycles_after_previous_change); /* In 8MHz units*/ + +void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback); +void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback); +void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback); +void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback); +void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback); +void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback); +void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback); +void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback); +/* Called when a new boot ROM is needed. The callback should call GB_load_boot_rom or GB_load_boot_rom_from_buffer */ +void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t callback); + +void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette); + +/* These APIs are used when using internal clock */ +void GB_set_serial_transfer_bit_start_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_start_callback_t callback); +void GB_set_serial_transfer_bit_end_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_end_callback_t callback); + +/* These APIs are used when using external clock */ +bool GB_serial_get_data_bit(GB_gameboy_t *gb); +void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data); + +void GB_disconnect_serial(GB_gameboy_t *gb); + +/* For cartridges with an alarm clock */ +unsigned GB_time_to_alarm(GB_gameboy_t *gb); // 0 if no alarm + +/* For integration with SFC/SNES emulators */ +void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback); +void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback); +void GB_set_icd_hreset_callback(GB_gameboy_t *gb, GB_icd_hreset_callback_t callback); +void GB_set_icd_vreset_callback(GB_gameboy_t *gb, GB_icd_vreset_callback_t callback); + +#ifdef GB_INTERNAL +uint32_t GB_get_clock_rate(GB_gameboy_t *gb); +#endif +void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier); + +unsigned GB_get_screen_width(GB_gameboy_t *gb); +unsigned GB_get_screen_height(GB_gameboy_t *gb); +double GB_get_usual_frame_rate(GB_gameboy_t *gb); +unsigned GB_get_player_count(GB_gameboy_t *gb); + +#endif /* GB_h */ diff --git a/bsnes/gb/Core/gb_struct_def.h b/bsnes/gb/Core/gb_struct_def.h new file mode 100644 index 00000000..0e0ebd12 --- /dev/null +++ b/bsnes/gb/Core/gb_struct_def.h @@ -0,0 +1,5 @@ +#ifndef gb_struct_def_h +#define gb_struct_def_h +struct GB_gameboy_s; +typedef struct GB_gameboy_s GB_gameboy_t; +#endif diff --git a/bsnes/gb/Core/graphics/agb_border.inc b/bsnes/gb/Core/graphics/agb_border.inc new file mode 100644 index 00000000..dd4ebbe8 --- /dev/null +++ b/bsnes/gb/Core/graphics/agb_border.inc @@ -0,0 +1,522 @@ +static const uint16_t palette[] = { + 0x410A, 0x0421, 0x35AD, 0x4A52, 0x7FFF, 0x2D49, 0x0C42, 0x1484, + 0x18A5, 0x20C6, 0x6718, 0x5D6E, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +static const uint16_t tilemap[] = { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0001, 0x0002, 0x0003, 0x0003, 0x0003, 0x0004, 0x0004, + 0x0004, 0x0004, 0x0004, 0x0004, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0004, 0x0004, 0x0004, 0x0004, + 0x0004, 0x0004, 0x0003, 0x0003, 0x0003, 0x4002, 0x4001, 0x0000, + 0x0000, 0x0006, 0x0007, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x4007, 0x4006, 0x0000, + 0x0000, 0x0009, 0x0008, 0x0008, 0x0008, 0x000A, 0x000B, 0x000B, + 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, + 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, 0x000B, + 0x000B, 0x000B, 0x400A, 0x0008, 0x0008, 0x0008, 0xC009, 0x0000, + 0x0000, 0x000C, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x400C, 0x0000, + 0x0000, 0x000E, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC00E, 0x0000, + 0x0000, 0x000F, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x400F, 0x0000, + 0x0000, 0x0010, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC010, 0x0000, + 0x0000, 0x0010, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC010, 0x0000, + 0x0000, 0x0011, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC011, 0x0000, + 0x0000, 0x0011, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC011, 0x0000, + 0x0000, 0x0012, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x4012, 0x0000, + 0x0000, 0x0013, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC013, 0x0000, + 0x0014, 0x0015, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x4015, 0x4014, + 0x0016, 0x0017, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC017, 0xC016, + 0x0016, 0x0017, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC017, 0xC016, + 0x0018, 0x0019, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x4019, 0x4018, + 0x001A, 0x001B, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0xC01B, 0xC01A, + 0x001C, 0x001D, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x401D, 0x401C, + 0x001E, 0x0008, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x0008, 0xC01E, + 0x001E, 0x0008, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x0008, 0xC01E, + 0x001E, 0x0008, 0x0008, 0x0008, 0x0008, 0x000D, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC00D, 0x0008, 0x0008, 0x0008, 0x0008, 0xC01E, + 0x001F, 0x801D, 0x0008, 0x0008, 0x0008, 0x0020, 0x0021, 0x0022, + 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, + 0x002B, 0x002C, 0x002D, 0x002D, 0x002D, 0x002D, 0x002D, 0x002D, + 0x002E, 0x0021, 0x4020, 0x0008, 0x0008, 0x0008, 0xC01D, 0x401F, + 0x002F, 0x0030, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0031, + 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, + 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, 0x0040, 0x0041, + 0x0042, 0x0043, 0x0008, 0x0008, 0x0008, 0x0008, 0x4030, 0x402F, + 0x0044, 0x0045, 0x0046, 0x0047, 0x0008, 0x0008, 0x0048, 0x0049, + 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050, 0x0051, + 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, + 0x005A, 0x005B, 0x0008, 0x0008, 0x4047, 0x4046, 0x4045, 0x4044, + 0x0000, 0x0000, 0x005C, 0x005D, 0x005E, 0x005F, 0x0060, 0x0061, + 0x0061, 0x0062, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, + 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x4062, 0x0061, + 0x0061, 0x4060, 0x405F, 0x405E, 0x405D, 0x405C, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, +}; + +const uint8_t tiles[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1F, 0x01, + 0x7F, 0x1F, 0xFF, 0x7E, 0xFF, 0xE1, 0xFF, 0x9F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x1E, + 0x1F, 0x60, 0x7F, 0x80, 0xFF, 0x00, 0xBF, 0x40, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x03, 0x01, 0x07, 0x03, 0x07, 0x03, 0x07, 0x06, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x01, 0x02, 0x03, 0x04, 0x03, 0x04, 0x06, 0x01, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0xFF, 0xBF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBF, 0x40, 0x7F, 0x80, 0x7F, 0x80, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, + 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, 0x0F, 0x0E, + 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, + 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, 0x0E, 0x01, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFE, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x0F, 0x0E, 0x0F, 0x0E, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x0E, 0x01, 0x0E, 0x01, 0x1B, 0x04, 0x1B, 0x04, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, + 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, + 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, + 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, 0x1F, 0x1B, + 0x1F, 0x1B, 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, + 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, 0x1B, 0x04, + 0x1B, 0x04, 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, + 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, + 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, 0x3F, 0x37, + 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, + 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, 0x37, 0x08, + 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, + 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, + 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, + 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, + 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0xFF, 0xDF, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0x6F, 0x10, 0x6F, 0x10, 0x6F, 0x10, 0xDF, 0x20, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xDF, + 0xFF, 0xDF, 0xFF, 0xDF, 0xFF, 0xBF, 0xFF, 0xBF, + 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, 0xDF, 0x20, + 0xDF, 0x20, 0xDF, 0x20, 0xBF, 0x40, 0xBF, 0x40, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, + 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xBF, + 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, + 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, 0xBF, 0x40, + 0x01, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x01, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0xFF, 0xBF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xBF, 0x40, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, 0x07, 0x06, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, 0x06, 0x01, + 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, + 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0x01, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF8, 0xFF, 0xE7, 0xF8, 0xDF, 0xE3, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xF8, 0x00, 0xE4, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0xFF, 0xCE, 0x3F, 0xF5, 0x8E, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0x00, 0x4E, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x1F, 0xFF, 0xEE, 0x1F, 0xB5, 0x4E, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x1F, 0x00, 0x0E, 0xA0, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x07, 0xFF, 0xFB, 0x07, 0x04, 0x73, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x07, 0x00, 0x03, 0x88, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x80, 0xFF, 0x7F, 0x80, 0x82, 0x39, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x80, 0x00, 0x01, 0x44, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x01, 0xFE, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0x83, 0x7C, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x42, 0x01, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xFF, 0xBB, 0x7C, 0x4F, 0xB0, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x7C, 0x00, 0xB1, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x07, 0xFF, 0xF9, 0x06, 0xE7, 0xF8, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x06, 0x00, 0x08, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x0E, 0xFF, 0xF5, 0x0E, 0x9B, 0x74, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x0E, 0x00, 0x94, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x1F, 0xFF, 0xF7, 0x0F, 0xBF, 0x47, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x0F, 0x00, 0xA7, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x03, 0x03, 0x03, 0x03, 0x01, 0x01, + 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, + 0x03, 0x04, 0x01, 0x02, 0x01, 0x02, 0x00, 0x01, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xDF, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0xBF, 0x40, 0xBF, 0x40, 0xDF, 0x20, + 0xB0, 0xD8, 0xA0, 0xD3, 0x67, 0x84, 0x47, 0xA4, + 0x61, 0x81, 0xA0, 0xD0, 0xB4, 0xCA, 0x7E, 0x81, + 0xD7, 0x08, 0xCC, 0x13, 0x98, 0x20, 0x98, 0x00, + 0x9E, 0x20, 0xCF, 0x00, 0xCD, 0x02, 0x80, 0x01, + 0x32, 0x2D, 0x13, 0x6D, 0x34, 0x48, 0xFC, 0x02, + 0x7C, 0x00, 0x78, 0x05, 0x30, 0x49, 0x20, 0x50, + 0xCD, 0x00, 0xAC, 0x40, 0x49, 0x82, 0x01, 0x02, + 0x07, 0x80, 0xC2, 0x05, 0x86, 0x41, 0x9F, 0x40, + 0x15, 0x2E, 0x09, 0x06, 0x09, 0x16, 0x0B, 0xD4, + 0xC6, 0x49, 0x8E, 0x40, 0xCF, 0xC8, 0x06, 0x01, + 0xCE, 0x20, 0xE6, 0x10, 0xE6, 0x00, 0x24, 0xD0, + 0x39, 0x80, 0x38, 0x01, 0x31, 0x00, 0xF8, 0x00, + 0x0C, 0x8B, 0x85, 0x8A, 0x03, 0x84, 0x27, 0x20, + 0x22, 0x35, 0x12, 0x34, 0x20, 0x12, 0x10, 0x20, + 0x73, 0x00, 0x72, 0x08, 0x7C, 0x80, 0xDC, 0x01, + 0xC8, 0x11, 0xC9, 0x06, 0xCD, 0x22, 0xEF, 0x10, + 0x83, 0x44, 0x86, 0x01, 0x03, 0x85, 0x26, 0x21, + 0x46, 0x69, 0x46, 0x68, 0x8E, 0xCA, 0x86, 0x88, + 0x39, 0x40, 0x78, 0x84, 0x7C, 0x80, 0xD8, 0x01, + 0x90, 0x29, 0xD1, 0x28, 0x73, 0x00, 0xB3, 0x40, + 0x00, 0x01, 0x01, 0x00, 0x3F, 0x00, 0x3F, 0x40, + 0x03, 0x02, 0x01, 0x02, 0x41, 0x7C, 0x7F, 0x00, + 0xFE, 0x00, 0xFF, 0x00, 0xC0, 0x00, 0x80, 0x40, + 0xFC, 0x00, 0xFC, 0x00, 0x80, 0x02, 0xC0, 0x00, + 0xC0, 0x00, 0x80, 0x4C, 0xCC, 0x43, 0x8E, 0x52, + 0x80, 0x4C, 0x80, 0x00, 0x12, 0x1E, 0x9E, 0x00, + 0x7F, 0x00, 0x33, 0x0C, 0x32, 0x01, 0x23, 0x50, + 0x33, 0x4C, 0x7F, 0x00, 0x61, 0x80, 0xF1, 0x00, + 0x7C, 0x02, 0x30, 0x48, 0x31, 0x40, 0x61, 0x50, + 0x87, 0xE4, 0xE3, 0x84, 0x23, 0x44, 0x43, 0x44, + 0x85, 0x42, 0x87, 0x40, 0x8F, 0x50, 0x8C, 0x12, + 0x78, 0x00, 0x18, 0x20, 0xB8, 0x00, 0x98, 0x24, + 0x03, 0x04, 0x03, 0xE0, 0xF1, 0x12, 0xF0, 0x09, + 0xF9, 0x09, 0xF9, 0x08, 0xE1, 0x12, 0xF1, 0x12, + 0xF8, 0x00, 0x1E, 0xE0, 0x0C, 0x02, 0x07, 0x08, + 0x07, 0x00, 0x06, 0x00, 0x1C, 0x02, 0x0C, 0x00, + 0x9F, 0x91, 0x86, 0x88, 0xC4, 0x4C, 0x80, 0x4C, + 0xE1, 0x20, 0xC1, 0x22, 0x23, 0xD4, 0x22, 0xD5, + 0x60, 0x00, 0xFB, 0x00, 0x37, 0x00, 0x73, 0x0C, + 0x1F, 0x00, 0x3C, 0x00, 0xC8, 0x14, 0xC9, 0x14, + 0x16, 0x2F, 0x76, 0x4F, 0x2D, 0xDE, 0xDD, 0xBE, + 0xBA, 0x7D, 0x7A, 0xFD, 0x7A, 0xFD, 0xF4, 0xF8, + 0xCF, 0x00, 0x8F, 0x00, 0x5E, 0x80, 0xBE, 0x00, + 0x7D, 0x00, 0xFC, 0x00, 0xFC, 0x01, 0xF9, 0x02, + 0xFF, 0x00, 0xBF, 0x78, 0x86, 0x09, 0x86, 0x89, + 0x06, 0x25, 0x02, 0x25, 0x42, 0x45, 0x60, 0x11, + 0x00, 0x00, 0x09, 0x00, 0x70, 0x81, 0x70, 0x09, + 0xDC, 0x21, 0xD8, 0x01, 0x98, 0x25, 0xCC, 0x13, + 0xFF, 0x00, 0xF3, 0xF8, 0x02, 0x03, 0x01, 0x30, + 0x39, 0x09, 0x30, 0x09, 0x31, 0x09, 0x20, 0x19, + 0x00, 0x00, 0x01, 0x04, 0xFC, 0x00, 0xCF, 0x30, + 0xE6, 0x00, 0xE6, 0x01, 0xE6, 0x00, 0xF6, 0x08, + 0xFF, 0x00, 0xFA, 0xC7, 0x18, 0x21, 0x09, 0x10, + 0x88, 0x99, 0x93, 0x1A, 0x83, 0x11, 0xC2, 0x41, + 0x00, 0x00, 0x00, 0x20, 0xC6, 0x21, 0xFF, 0x00, + 0x67, 0x00, 0xE4, 0x08, 0x6F, 0x10, 0x3C, 0x00, + 0xFD, 0x02, 0xB5, 0x3A, 0xC7, 0x44, 0x03, 0x84, + 0x83, 0x24, 0x21, 0xB0, 0x21, 0x12, 0x21, 0x02, + 0x02, 0x00, 0x02, 0x40, 0x3C, 0x00, 0xF8, 0x00, + 0xD8, 0x24, 0x4C, 0x92, 0xEC, 0x00, 0xCC, 0x12, + 0xFF, 0x00, 0xFF, 0xF3, 0x1C, 0x14, 0x0C, 0x04, + 0x00, 0x0C, 0x04, 0x24, 0x00, 0x24, 0x10, 0x30, + 0x00, 0x00, 0x10, 0x04, 0xE3, 0x00, 0xFB, 0x00, + 0xF3, 0x08, 0xDB, 0x20, 0xDB, 0x04, 0xCF, 0x00, + 0xFF, 0x00, 0xEC, 0x3E, 0xC1, 0x01, 0x01, 0x8E, + 0x8F, 0x10, 0x0F, 0x90, 0x0F, 0x90, 0x0D, 0x09, + 0x00, 0x00, 0x20, 0x01, 0x7E, 0x00, 0xF1, 0x0E, + 0xE0, 0x10, 0x60, 0x10, 0x60, 0x10, 0x79, 0x82, + 0xFF, 0x00, 0x7F, 0xFC, 0x03, 0x82, 0x01, 0x9E, + 0x13, 0x80, 0x03, 0x80, 0x03, 0x9C, 0x0F, 0x90, + 0x00, 0x00, 0x02, 0x00, 0x7C, 0x80, 0x60, 0x9C, + 0x60, 0x9C, 0x7C, 0x80, 0x60, 0x9C, 0x70, 0x80, + 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, + 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xEF, 0xFF, 0xF7, 0x7F, 0x7B, 0x3F, 0x3C, + 0x1F, 0x1F, 0x0F, 0x0F, 0x03, 0x03, 0x00, 0x00, + 0xEF, 0x10, 0x77, 0x88, 0x3B, 0x44, 0x1C, 0x23, + 0x0F, 0x10, 0x03, 0x0C, 0x00, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x3F, 0xFF, 0xC3, 0xFF, 0xFC, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x3F, 0xC0, 0xC3, 0x3C, 0xFC, 0x03, 0x3F, 0xC0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xC1, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0xC0, 0xC1, 0x3E, + 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xEA, 0x14, 0xC0, 0x00, 0x80, 0x21, 0x7F, 0x92, + 0x9F, 0xE0, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x27, 0x18, 0x7F, 0x00, 0x1E, 0x61, 0x9A, 0x04, + 0xE0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x73, 0x53, 0x47, 0x44, 0x46, 0x25, 0xFD, 0x03, + 0xF9, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x8C, 0x00, 0xD8, 0x20, 0x1D, 0xA0, 0x03, 0x00, + 0x07, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC7, 0xE1, 0xE6, 0x05, 0x42, 0xA5, 0xBF, 0xC0, + 0x9F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x18, 0x24, 0x38, 0x01, 0xB8, 0x05, 0xC0, 0x00, + 0xE0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x21, 0x11, 0x31, 0x49, 0x33, 0x4A, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xDE, 0x00, 0x87, 0x48, 0x84, 0x48, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xCC, 0x02, 0x8E, 0x4A, 0xCC, 0x42, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x71, 0x08, 0x39, 0x00, 0x31, 0x02, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x3D, 0x40, 0x03, 0x02, 0x03, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBC, 0x02, 0xFC, 0x00, 0xFE, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x12, 0x82, 0x80, 0x80, 0x01, 0x83, 0xFF, 0x00, + 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x61, 0x1C, 0x7F, 0x00, 0x7C, 0x82, 0x00, 0x00, + 0x01, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x22, 0x52, 0x30, 0xC0, 0x58, 0xA4, 0x8F, 0x72, + 0x73, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x8C, 0x11, 0x4F, 0x90, 0xA3, 0x0C, 0x73, 0x00, + 0xFC, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x23, 0xA4, 0x06, 0x0D, 0x05, 0x1B, 0xBB, 0x07, + 0xE7, 0x1F, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x98, 0x44, 0xF5, 0x08, 0xEB, 0x00, 0x87, 0x40, + 0x1F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x66, 0x85, 0xE2, 0xA5, 0x66, 0x81, 0xBF, 0xC1, + 0x99, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x99, 0x00, 0xB9, 0x00, 0x9D, 0x20, 0xC1, 0x00, + 0xE7, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xF6, 0xFA, 0xFC, 0xF2, 0xF7, 0xF8, 0xFB, 0xFC, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF9, 0x00, 0xF1, 0x02, 0xF8, 0x00, 0xFC, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x52, 0x53, 0x30, 0x23, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x8C, 0x21, 0xCC, 0x13, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x03, 0x03, 0x06, 0xFE, 0x01, 0xF9, 0x07, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0x02, 0xFA, 0x04, 0x01, 0x00, 0x07, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x86, 0x05, 0x46, 0xA0, 0x5F, 0xB8, 0xBF, 0xC0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x38, 0x41, 0x99, 0x26, 0xB8, 0x00, 0xC0, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x30, 0x28, 0x09, 0x09, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC6, 0x09, 0xE6, 0x10, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x20, 0x38, 0x38, 0x20, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xD7, 0x08, 0xCF, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x80, 0x41, 0xA1, 0x61, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3E, 0x40, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x01, 0x82, 0x01, 0x82, 0xFF, 0x00, 0xFC, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7C, 0x82, 0x7C, 0x82, 0x00, 0x00, 0x03, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x3F, 0x3F, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x3C, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFE, 0xFF, 0xFF, 0x3F, 0x3F, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFE, 0x01, 0x3F, 0xC0, 0x01, 0x3E, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x3F, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0xC0, 0xC0, 0x3F, 0xFF, 0x00, 0x3F, 0xC0, + 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xC0, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0x3F, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x3F, 0xC0, 0xC0, 0x3F, 0xFF, 0x00, + 0x3F, 0xC0, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFC, + 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x03, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0x03, 0xFC, 0xFC, 0x03, + 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x03, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, + 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x03, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x03, 0xFC, + 0xFC, 0x03, 0xFF, 0x00, 0x03, 0xFC, 0x00, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, +}; diff --git a/bsnes/gb/Core/graphics/cgb_border.inc b/bsnes/gb/Core/graphics/cgb_border.inc new file mode 100644 index 00000000..755312a4 --- /dev/null +++ b/bsnes/gb/Core/graphics/cgb_border.inc @@ -0,0 +1,446 @@ +static const uint16_t palette[] = { + 0x7C1A, 0x0000, 0x0011, 0x3DEF, 0x6318, 0x7FFF, 0x1EBA, 0x19AF, + 0x1EAF, 0x4648, 0x7FC0, 0x2507, 0x1484, 0x5129, 0x5010, 0x2095, +}; + +static const uint16_t tilemap[] = { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0001, 0x0002, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, + 0x0003, 0x0003, 0x0003, 0x0003, 0x0003, 0x4002, 0x4001, 0x0000, + 0x0000, 0x0004, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x4004, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0007, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, 0x0008, + 0x0008, 0x0008, 0x4007, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x000A, 0x000B, 0x400A, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x800A, 0x000C, 0xC00A, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x0006, 0x0005, 0x0005, 0x0005, 0x0009, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC009, 0x0005, 0x0005, 0x0005, 0xC006, 0x0000, + 0x0000, 0x000D, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x000E, + 0x000F, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, + 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, + 0x001F, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x400D, 0x0000, + 0x0000, 0x0020, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0021, + 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, + 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, 0x0030, 0x0031, + 0x0032, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x4020, 0x0000, + 0x0000, 0x0033, 0x0034, 0x0035, 0x0036, 0x0005, 0x0005, 0x0037, + 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, + 0x0040, 0x0005, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, + 0x0047, 0x0005, 0x0005, 0x4036, 0x4035, 0x4034, 0x4033, 0x0000, + 0x0000, 0x0000, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, + 0x004E, 0x004E, 0x004F, 0x0050, 0x0050, 0x0050, 0x0050, 0x0050, + 0x0050, 0x0050, 0x0050, 0x0050, 0x0050, 0x404F, 0x004E, 0x004E, + 0x404D, 0x004C, 0x404B, 0x404A, 0x4049, 0x4048, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 +}; + +const uint8_t tiles[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x08, + 0x01, 0x11, 0x06, 0x26, 0x04, 0x24, 0x08, 0x48, + 0x00, 0x00, 0x01, 0x01, 0x07, 0x07, 0x0F, 0x0F, + 0x1E, 0x1F, 0x39, 0x3F, 0x3B, 0x3F, 0x77, 0x7F, + 0x00, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x7F, 0x7F, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x08, 0x48, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x77, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x42, + 0xBD, 0xBD, 0x7E, 0x66, 0x7E, 0xFF, 0x7E, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBD, 0xFF, + 0x7E, 0xFF, 0xFF, 0xE7, 0x7E, 0x7E, 0x7E, 0x7E, + 0x7E, 0xFF, 0x3C, 0xFF, 0x81, 0xFF, 0xC3, 0xFF, + 0x7E, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7E, 0x7E, 0x3C, 0x3C, 0x00, 0x00, 0x00, 0x81, + 0x81, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, + 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, 0x6F, 0x7F, + 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFE, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xDF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x78, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, 0xC7, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0xE3, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x9F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x60, 0xE1, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8, 0xD8, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x8F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0x8F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x70, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0x9F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x0F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC3, 0xC2, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBD, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x10, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDF, 0xE0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0x84, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x08, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, + 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, 0x08, 0x48, + 0x08, 0x48, 0x04, 0x24, 0x04, 0x24, 0x02, 0x12, + 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, 0x37, 0x7F, + 0x37, 0x7F, 0x1B, 0x3F, 0x1B, 0x3F, 0x0D, 0x1F, + 0x0F, 0x08, 0x0E, 0x00, 0x1E, 0x12, 0x1E, 0x12, + 0x1F, 0x10, 0x0F, 0x08, 0x02, 0x02, 0x00, 0x00, + 0xF7, 0xF8, 0xFF, 0xF0, 0xED, 0xE3, 0xED, 0xE1, + 0xEF, 0xE0, 0xF7, 0xF0, 0xFD, 0xFC, 0xFF, 0xFF, + 0xF0, 0x10, 0x40, 0x00, 0x41, 0x41, 0x00, 0x00, + 0x83, 0x82, 0xE3, 0x20, 0xC7, 0x04, 0xC7, 0x00, + 0xEF, 0x1F, 0xFF, 0x1F, 0xBE, 0xFF, 0xFF, 0xFE, + 0x7D, 0x7E, 0xDF, 0x3C, 0xFB, 0x18, 0xFF, 0x18, + 0x60, 0x00, 0x70, 0x00, 0xF8, 0x08, 0xB0, 0x00, + 0xD8, 0x40, 0x3C, 0x24, 0x5C, 0x44, 0xFC, 0x00, + 0xFF, 0x8F, 0xFF, 0x0F, 0xF7, 0x07, 0xFF, 0x07, + 0xBF, 0x47, 0xDB, 0x47, 0xBB, 0x03, 0xFF, 0x03, + 0x3C, 0x04, 0x78, 0x00, 0x78, 0x00, 0xEC, 0x80, + 0xFE, 0x92, 0xE7, 0x83, 0xE5, 0x80, 0x4F, 0x08, + 0xFB, 0x83, 0xFF, 0x83, 0xFF, 0x83, 0x7F, 0x83, + 0x6D, 0x93, 0x7C, 0x10, 0x7F, 0x10, 0xF7, 0x10, + 0x3C, 0x00, 0x7C, 0x40, 0x78, 0x00, 0xC8, 0x80, + 0xFC, 0x24, 0xBC, 0x24, 0xFD, 0x65, 0x3D, 0x25, + 0xFF, 0xC3, 0xBF, 0x83, 0xFF, 0x83, 0x7F, 0x03, + 0xDB, 0x23, 0xDB, 0x23, 0x9A, 0x67, 0xDA, 0x47, + 0xFF, 0x80, 0xFF, 0x80, 0xE0, 0x80, 0x40, 0x00, + 0xFF, 0x01, 0xFF, 0x01, 0xDF, 0x1F, 0xE0, 0x20, + 0x7F, 0x80, 0x7F, 0x00, 0x7F, 0x1F, 0xFF, 0x1F, + 0xFE, 0x00, 0xFE, 0x00, 0xE0, 0x01, 0xDF, 0x3F, + 0xBF, 0xA0, 0xB9, 0xA0, 0x10, 0x00, 0x11, 0x01, + 0x3B, 0x00, 0x3F, 0x00, 0x7E, 0x4E, 0x78, 0x48, + 0x5F, 0x40, 0x5F, 0xC0, 0xFF, 0xC7, 0xFE, 0xC7, + 0xFF, 0xC0, 0xFF, 0xC0, 0xB1, 0xC4, 0xB7, 0x8F, + 0xE3, 0x22, 0xC7, 0x04, 0xCE, 0x08, 0xE6, 0x20, + 0xCE, 0x42, 0xDE, 0x52, 0xFE, 0x32, 0xFC, 0x30, + 0xDD, 0x3E, 0xFB, 0x18, 0xF7, 0x18, 0xDF, 0x31, + 0xBD, 0x31, 0xAD, 0x23, 0xCD, 0x31, 0xCF, 0x11, + 0xFE, 0x02, 0x9E, 0x00, 0x86, 0x80, 0x06, 0x00, + 0x03, 0x00, 0x02, 0x00, 0x07, 0x01, 0x07, 0x01, + 0xFD, 0x03, 0xFF, 0x01, 0x7F, 0xF0, 0xFF, 0xF8, + 0xFF, 0xF8, 0xFF, 0xF8, 0xFE, 0xF8, 0xFE, 0xF1, + 0x38, 0x08, 0x71, 0x41, 0x1F, 0x06, 0x39, 0x20, + 0x0F, 0x00, 0x0F, 0x01, 0x04, 0x00, 0x0C, 0x00, + 0xF7, 0x87, 0xBE, 0xC6, 0xF9, 0xC6, 0xDF, 0xE0, + 0xFF, 0xE0, 0xFE, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, + 0x70, 0x10, 0xE0, 0x20, 0x80, 0x00, 0x80, 0x00, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xEF, 0x1F, 0xDF, 0x1F, 0xFF, 0x3F, 0xFF, 0x7F, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3F, 0x3F, 0x7F, 0x7F, 0x78, 0x78, 0xF0, 0xF0, + 0xF0, 0xF0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF0, 0xF0, + 0xDF, 0xFF, 0xBD, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, + 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0xFF, + 0xE7, 0xE0, 0xFF, 0xF0, 0xFE, 0xF0, 0x1C, 0x00, + 0x3C, 0x20, 0x3C, 0x24, 0x3C, 0x24, 0x3C, 0x20, + 0xFF, 0xFF, 0xEF, 0xFF, 0x6F, 0xFF, 0xFF, 0xFF, + 0xDF, 0xFF, 0xDB, 0xFF, 0xDB, 0xFF, 0xDF, 0xFF, + 0xF8, 0x00, 0xFC, 0xC0, 0x1F, 0x03, 0x1F, 0x13, + 0x1F, 0x13, 0x1E, 0x02, 0x1E, 0x02, 0x3E, 0x02, + 0xFF, 0xFF, 0x3F, 0xFF, 0xFC, 0xFF, 0xEC, 0xFF, + 0xED, 0xFE, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, + 0x90, 0x90, 0x00, 0x00, 0x00, 0x00, 0x21, 0x21, + 0x20, 0x21, 0x00, 0x01, 0x00, 0x01, 0x40, 0x41, + 0x8F, 0x7F, 0x1F, 0xFF, 0x1F, 0xFF, 0x3F, 0xDE, + 0x1F, 0xFE, 0x3F, 0xFE, 0x3F, 0xFE, 0x7F, 0xBE, + 0x40, 0x7F, 0x84, 0xFF, 0x11, 0xF1, 0x20, 0xE0, + 0x20, 0xE0, 0x01, 0xC1, 0x01, 0xC1, 0x22, 0xE3, + 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x0E, 0xFF, 0x1F, + 0xDF, 0x3F, 0xFE, 0x3F, 0xFF, 0x3E, 0xFD, 0x1E, + 0x47, 0xC0, 0x27, 0xE0, 0x2F, 0xE8, 0x0F, 0xE9, + 0x0F, 0xE1, 0x0F, 0xE0, 0x3F, 0xF0, 0x3F, 0xF1, + 0xF8, 0x3F, 0xF8, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, + 0xF0, 0x1F, 0xF0, 0x1F, 0xE0, 0x1F, 0xC0, 0x3F, + 0xFC, 0x00, 0xFE, 0xE2, 0x1E, 0x12, 0x1E, 0x12, + 0x3E, 0x22, 0xFC, 0x00, 0xF8, 0x08, 0xF0, 0x10, + 0x03, 0xFF, 0x01, 0xFF, 0xE1, 0xFF, 0xE1, 0xFF, + 0xC1, 0xFF, 0x03, 0xFF, 0x07, 0xFF, 0x0F, 0xFF, + 0x01, 0x11, 0x00, 0x08, 0x00, 0x04, 0x00, 0x02, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x1F, 0x07, 0x0F, 0x03, 0x07, 0x01, 0x03, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x80, 0x40, 0x40, 0x30, 0x30, + 0x0C, 0x0C, 0x03, 0xC3, 0x00, 0x30, 0x00, 0x0C, + 0xFF, 0xFF, 0x7F, 0xFF, 0xBF, 0xFF, 0xCF, 0xFF, + 0xF3, 0xFF, 0x3C, 0xFF, 0x0F, 0x3F, 0x03, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0xC0, 0x3C, 0x3C, 0x03, 0x03, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0xFF, 0xC3, 0xFF, 0xFC, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xE0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, + 0x15, 0x15, 0x3F, 0x20, 0x2F, 0x20, 0x06, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xEA, 0xE6, 0xDF, 0xC0, 0xDF, 0xE0, 0xF9, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xE6, 0x20, 0x9E, 0x12, 0x0C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xDF, 0x30, 0xED, 0x31, 0xFF, 0x63, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x0E, 0x02, 0x1E, 0x12, 0x0D, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFD, 0x03, 0xED, 0xE1, 0xFE, 0xF1, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x4F, 0x08, 0xE6, 0x20, 0xE7, 0x21, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF7, 0x18, 0xDF, 0x18, 0xDE, 0x18, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xB9, 0xA1, 0x11, 0x01, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x5E, 0x46, 0xFE, 0xC6, 0xFF, 0xC6, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x3F, 0xFF, 0x01, 0xFF, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0x01, 0xFE, 0x00, 0xFE, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7E, 0x4E, 0x3F, 0x00, 0x3E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xB1, 0x8E, 0xFF, 0x80, 0xFF, 0x80, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xEE, 0x20, 0x8F, 0x08, 0x85, 0x84, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xDF, 0x30, 0xF7, 0x38, 0x7B, 0x7C, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xAE, 0xA2, 0xF8, 0x00, 0xE8, 0x08, 0xC0, 0xC0, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x5D, 0xE1, 0xFF, 0x03, 0xF7, 0x0F, 0x3F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x0E, 0x02, 0x1E, 0x12, 0x1E, 0x12, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFD, 0xF1, 0xED, 0xF1, 0xED, 0xE3, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFB, 0xFB, 0x7F, 0x7F, 0x3F, 0x3F, 0x0C, 0x0C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x75, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xDF, 0xC1, 0xDF, 0xD0, 0x8F, 0x88, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFE, 0xFF, 0xEF, 0xFF, 0x77, 0xFF, 0xFE, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFA, 0x82, 0xF8, 0x08, 0xE0, 0x00, 0x81, 0x81, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7E, 0xFD, 0xF4, 0xFF, 0xFC, 0xFF, 0x7E, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7D, 0x7D, 0x02, 0x02, 0x02, 0x02, 0xFC, 0xFC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xFE, 0x01, 0xFF, 0x01, 0xFF, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x1C, 0xFF, 0x00, 0xFF, 0x41, 0x7F, 0x18, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xF7, 0x08, 0xFF, 0x00, 0xFF, 0x80, 0xE7, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x5E, 0xC2, 0x9C, 0x80, 0x1C, 0x04, 0x08, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xE1, 0x3F, 0xE3, 0x7F, 0xE3, 0xFF, 0xF7, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF0, 0x80, 0x78, 0x08, 0x78, 0x48, 0x10, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0xFF, 0x87, 0xFF, 0x87, 0xFF, 0xEF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0x00, 0x3C, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3F, 0xFF, 0x03, 0x3F, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1C, 0x1C, 0x03, 0x03, 0x00, 0xE0, 0x00, 0x1C, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xE3, 0xFF, 0xFC, 0xFF, 0x1F, 0xFF, 0x03, 0x1F, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFC, 0xFC, 0x03, 0x03, 0x00, 0x00, + 0x00, 0xFC, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, + 0x03, 0xFF, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0x3F, 0x3F, + 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xC0, 0xFF, + 0xFF, 0xFF, 0x3F, 0xFF, 0x00, 0x3F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, + 0x3F, 0x3F, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x3F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, + 0xC0, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0x00, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF +}; diff --git a/bsnes/gb/Core/graphics/dmg_border.inc b/bsnes/gb/Core/graphics/dmg_border.inc new file mode 100644 index 00000000..7db0673a --- /dev/null +++ b/bsnes/gb/Core/graphics/dmg_border.inc @@ -0,0 +1,558 @@ +static const uint16_t palette[] = { + 0x0000, 0x0011, 0x18C6, 0x001A, 0x318C, 0x39CE, 0x5294, 0x5AD6, + 0x739C, 0x45A8, 0x4520, 0x18A5, 0x4A32, 0x2033, 0x20EC, 0x0000, +}; + +static const uint16_t tilemap[] = { + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, + 0x0001, 0x0003, 0x0004, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, + 0x0005, 0x0005, 0x0005, 0x0005, 0x0005, 0x4004, 0x4003, 0x0001, + 0x0001, 0x0006, 0x0007, 0x0007, 0x0007, 0x0008, 0x0009, 0x000A, + 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0010, 0x0011, 0x0012, + 0x0013, 0x0014, 0x0015, 0x000E, 0x0016, 0x0017, 0x0018, 0x0019, + 0x001A, 0x001B, 0x001C, 0x0007, 0x0007, 0x0007, 0x4006, 0x0001, + 0x0001, 0x001D, 0x001E, 0x001E, 0x001E, 0x001F, 0x0020, 0x0021, + 0x0022, 0x0023, 0x0024, 0x0025, 0x4024, 0x0026, 0x0025, 0x0025, + 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, + 0x002F, 0x0030, 0x0031, 0x001E, 0x001E, 0x001E, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0034, 0x0035, 0x4034, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x8034, 0x0036, 0xC034, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0xC01D, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0x0037, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0032, 0x0038, 0x0001, + 0x0001, 0x001D, 0x0032, 0x0032, 0x0032, 0x0033, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0xC033, 0x0032, 0x0032, 0x0039, 0x003A, 0x0001, + 0x0001, 0x003B, 0x003C, 0x0032, 0x0032, 0xC03C, 0x003D, 0x003D, + 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, + 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, 0x003D, + 0x003D, 0x003D, 0x003E, 0x003F, 0x0040, 0x0041, 0x0001, 0x0001, + 0x0001, 0x0042, 0x0043, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, + 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, + 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, + 0x0044, 0x0044, 0x0045, 0x0046, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, + 0x004E, 0x004F, 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x005B, + 0x005C, 0x005D, 0x005E, 0x005F, 0x0060, 0x0061, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, + 0x0068, 0x0069, 0x006A, 0x006B, 0x0001, 0x006C, 0x0001, 0x0001, + 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, +}; + +const uint8_t tiles[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0x01, 0xFE, 0x06, 0xF9, 0x08, 0xF7, + 0x11, 0xEF, 0x22, 0xDB, 0x20, 0xDB, 0x40, 0xB7, + 0xFF, 0x00, 0xFF, 0x00, 0xFE, 0x00, 0xF8, 0x00, + 0xF1, 0x00, 0xE6, 0x04, 0xE4, 0x00, 0xC8, 0x00, + 0x7F, 0x80, 0x80, 0x7F, 0x00, 0xFF, 0x7F, 0xFF, + 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x80, 0x00, 0x00, 0x00, 0x7F, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0xB7, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0xC8, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x03, 0xFF, 0x02, 0xFF, + 0x02, 0xFF, 0x02, 0xFF, 0x02, 0xFF, 0x02, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xC1, 0xDD, 0x00, 0xC9, + 0x14, 0xFF, 0x14, 0xFF, 0x14, 0xFF, 0x00, 0xC9, + 0x00, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x36, 0x22, + 0x14, 0x00, 0x14, 0x00, 0x14, 0x00, 0x36, 0x22, + 0x00, 0xFF, 0x00, 0xFF, 0xC7, 0xDF, 0x01, 0xCF, + 0x11, 0xFF, 0x11, 0xFF, 0x11, 0xFF, 0x01, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x31, 0x20, + 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x31, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0xC2, 0xFF, 0x03, 0xFF, + 0x02, 0xFE, 0x02, 0xFE, 0x02, 0xFF, 0x02, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xC2, 0x00, 0x03, 0x00, + 0x03, 0x01, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xFF, 0x18, 0xFF, + 0x08, 0x4E, 0x08, 0x4E, 0x09, 0x1F, 0x08, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x18, 0x00, + 0xB9, 0x10, 0xB9, 0xA1, 0xE9, 0xA0, 0xEB, 0x41, + 0x00, 0xFF, 0x00, 0xFF, 0x4F, 0xFF, 0x02, 0x1F, + 0x02, 0x4F, 0x02, 0x4F, 0xF2, 0xFF, 0x02, 0xE7, + 0x00, 0x00, 0x00, 0x00, 0x4F, 0x00, 0xE2, 0xA0, + 0xB2, 0xA0, 0xB2, 0x10, 0xF2, 0x00, 0x1A, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0xBC, 0xFD, 0x22, 0xFB, + 0x22, 0xFB, 0x3C, 0xFD, 0x24, 0xFF, 0x26, 0xF9, + 0x00, 0x00, 0x00, 0x00, 0xBE, 0x00, 0x26, 0x00, + 0x26, 0x00, 0x3E, 0x00, 0x24, 0x00, 0x26, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x50, 0xFF, 0x49, 0xEF, + 0x49, 0xF0, 0x46, 0xFF, 0x49, 0xF0, 0x49, 0xEF, + 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x59, 0x00, + 0x4F, 0x06, 0x46, 0x00, 0x4F, 0x06, 0x59, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x88, 0xFF, 0x00, 0x72, + 0x00, 0xF2, 0x05, 0xFF, 0x00, 0xF8, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x88, 0x00, 0x8D, 0x08, + 0x0D, 0x05, 0x05, 0x00, 0x07, 0x05, 0x87, 0x02, + 0x00, 0xFF, 0x00, 0xFF, 0x8A, 0xFF, 0x02, 0x27, + 0x02, 0x27, 0x52, 0xFF, 0x02, 0x8F, 0x02, 0x8F, + 0x00, 0x00, 0x00, 0x00, 0x8A, 0x00, 0xDA, 0x88, + 0xDA, 0x50, 0x52, 0x00, 0x72, 0x50, 0x72, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0xFA, 0xFF, 0x22, 0xFF, + 0x22, 0xFF, 0x23, 0xFF, 0x22, 0xFF, 0x22, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x22, 0x00, + 0x22, 0x00, 0x23, 0x00, 0x22, 0x00, 0x22, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x20, 0xFF, 0x20, 0xFF, + 0x20, 0xFF, 0xE0, 0xFF, 0x20, 0xFF, 0x20, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, + 0x20, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x20, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x33, 0x37, 0x00, 0x77, + 0x80, 0xFF, 0x20, 0x27, 0x08, 0xFF, 0x00, 0x77, + 0x00, 0x00, 0x00, 0x00, 0xFB, 0x40, 0x88, 0x88, + 0x80, 0x00, 0xF8, 0x50, 0x08, 0x00, 0x88, 0x88, + 0x00, 0xFF, 0x00, 0xFF, 0xEF, 0xFF, 0x88, 0xFF, + 0x88, 0xFF, 0x8F, 0xFF, 0x88, 0xFF, 0x88, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xEF, 0x00, 0x88, 0x00, + 0x88, 0x00, 0x8F, 0x00, 0x88, 0x00, 0x88, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xF9, 0xFD, 0x80, 0xF9, + 0x84, 0xFF, 0xF4, 0xFF, 0x84, 0xFF, 0x80, 0xF9, + 0x00, 0x00, 0x00, 0x00, 0xFB, 0x00, 0x86, 0x02, + 0x84, 0x00, 0xF4, 0x00, 0x84, 0x00, 0x86, 0x02, + 0x00, 0xFF, 0x00, 0xFF, 0xC0, 0xDF, 0x00, 0xCF, + 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x00, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x30, 0x20, + 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x30, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0x30, 0x36, 0x00, 0x74, + 0x82, 0xFF, 0x22, 0x27, 0x0A, 0xFF, 0x00, 0x74, + 0x00, 0x00, 0x00, 0x00, 0xF9, 0x40, 0x8B, 0x89, + 0x82, 0x00, 0xFA, 0x50, 0x0A, 0x00, 0x8B, 0x89, + 0x00, 0xFF, 0x00, 0xFF, 0xE2, 0xEF, 0x02, 0xE7, + 0x0A, 0xFF, 0x0A, 0xFF, 0x0A, 0xFF, 0x00, 0xE4, + 0x00, 0x00, 0x00, 0x00, 0xF2, 0x00, 0x1A, 0x10, + 0x0A, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x1B, 0x12, + 0x00, 0xFF, 0x00, 0xFF, 0x14, 0xFF, 0x16, 0xFF, + 0x14, 0xFC, 0x15, 0xFE, 0x14, 0xFF, 0x04, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x16, 0x00, + 0x17, 0x01, 0x15, 0x00, 0x14, 0x00, 0x34, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0x2F, 0xFF, 0x28, 0xFF, + 0x28, 0xFF, 0xA8, 0x7F, 0x28, 0x3F, 0x68, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x28, 0x00, + 0x28, 0x00, 0xA8, 0x00, 0xE8, 0x80, 0x68, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0x3F, + 0x40, 0xFF, 0x40, 0xFF, 0x40, 0xFF, 0x00, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x80, + 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xC0, 0x80, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xC1, 0xDD, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC1, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x02, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x4A, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x0A, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x22, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x82, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x20, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x60, 0x67, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF8, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x8F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x8F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xA2, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xF9, 0xFD, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC0, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x60, 0x66, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF9, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xE0, 0xEE, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC4, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE4, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x2F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0xFF, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, + 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x3C, 0xFF, + 0x7E, 0xFF, 0xE7, 0xE7, 0xFF, 0x7E, 0xFF, 0x7E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x7E, + 0x81, 0xC3, 0x00, 0x99, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x7E, 0xFF, 0x3C, 0xFF, 0x00, 0x7E, 0x81, + 0x3C, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0xC3, 0x81, + 0x7E, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF7, 0x00, 0xF7, 0x00, 0xF7, 0x00, 0xF7, + 0x00, 0xF7, 0x00, 0xED, 0x00, 0xED, 0x00, 0xED, + 0x09, 0x00, 0x09, 0x00, 0x09, 0x00, 0x09, 0x00, + 0x09, 0x00, 0x11, 0x02, 0x11, 0x02, 0x11, 0x02, + 0x00, 0xED, 0x00, 0xDB, 0x00, 0xDB, 0x00, 0xDB, + 0x00, 0xB7, 0x00, 0xB7, 0x00, 0x6F, 0x00, 0x6F, + 0x11, 0x02, 0x23, 0x04, 0x23, 0x04, 0x23, 0x04, + 0x47, 0x08, 0x47, 0x08, 0x8F, 0x10, 0x8F, 0x10, + 0x00, 0xFE, 0x00, 0xFD, 0x00, 0xFB, 0x00, 0xF7, + 0x00, 0xEE, 0x00, 0xDD, 0x00, 0xBB, 0x00, 0x77, + 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x08, 0x00, + 0x10, 0x01, 0x21, 0x02, 0x43, 0x04, 0x87, 0x08, + 0x00, 0xDF, 0x00, 0xBF, 0x00, 0xBF, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x1F, 0x20, 0x3F, 0x40, 0x3F, 0x40, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xB7, + 0x00, 0xB7, 0x00, 0xDB, 0x00, 0xDD, 0x00, 0xEE, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x88, 0x40, + 0x88, 0x40, 0xC4, 0x20, 0xC2, 0x20, 0xE1, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFC, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFC, 0x00, 0xE3, 0x00, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x1C, 0x00, 0xE0, 0x00, + 0x00, 0xFE, 0x00, 0xFD, 0x00, 0xF3, 0x00, 0xEF, + 0x00, 0x1C, 0x00, 0xF3, 0x00, 0xEF, 0x00, 0x1F, + 0x01, 0x00, 0x02, 0x00, 0x0C, 0x00, 0x10, 0x00, + 0xE0, 0x03, 0x03, 0x0C, 0x0F, 0x10, 0x1F, 0xE0, + 0x00, 0xEF, 0x00, 0xDF, 0x00, 0xBF, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x0F, 0x10, 0x1F, 0x20, 0x3F, 0x40, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xF7, 0x00, 0xF9, 0x00, 0xFE, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xF0, 0x08, 0xF8, 0x06, 0xFE, 0x01, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x80, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x7F, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x03, 0x00, 0xFF, 0x00, 0xFC, 0x00, 0x03, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0xFC, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFC, 0x00, 0xE3, 0x00, 0x1F, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x03, 0x03, 0x1C, 0x1F, 0xE0, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x21, 0xDE, 0x00, 0x7F, + 0x0C, 0xF3, 0x19, 0xE0, 0x10, 0xEE, 0x08, 0xF7, + 0xFF, 0x00, 0xFF, 0x00, 0xC0, 0x3F, 0x80, 0xFF, + 0x00, 0xFF, 0x0E, 0xF7, 0x1F, 0xE1, 0x07, 0xF8, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0xBF, + 0x40, 0xBE, 0x80, 0x3F, 0x02, 0xFD, 0x00, 0xFB, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, 0xC0, + 0x7F, 0x81, 0xFE, 0x41, 0xFC, 0x03, 0xFC, 0x07, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xF7, 0x00, 0xFF, + 0x00, 0xFB, 0x04, 0xFB, 0x24, 0xDB, 0x64, 0x9B, + 0xFF, 0x00, 0xFF, 0x00, 0x87, 0x78, 0x07, 0xF8, + 0x07, 0xFC, 0x07, 0xF8, 0x03, 0xFC, 0x43, 0xBC, + 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xDE, 0x20, 0xDF, + 0x20, 0xDF, 0x00, 0xFF, 0x04, 0xFB, 0x04, 0xFB, + 0xFF, 0x00, 0xFF, 0x00, 0xE0, 0x3F, 0xC0, 0x3F, + 0xC0, 0x3F, 0xC0, 0x3F, 0xC0, 0x3F, 0xC0, 0x3F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, 0x00, 0xFF, + 0x00, 0x77, 0x00, 0x7F, 0x80, 0x6F, 0x82, 0x7D, + 0xFF, 0x00, 0xFF, 0x00, 0xFC, 0x07, 0xF8, 0x07, + 0xF8, 0x8F, 0xF0, 0x8F, 0x70, 0x9F, 0x60, 0x9F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, 0x24, 0xCB, + 0x24, 0xDB, 0x20, 0xDF, 0x20, 0xDF, 0x00, 0xDF, + 0xFF, 0x00, 0xFF, 0x00, 0x1C, 0xE7, 0x18, 0xF7, + 0x18, 0xE7, 0x18, 0xE7, 0x38, 0xC7, 0x38, 0xE7, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x02, 0xFC, + 0x7E, 0x81, 0x80, 0x01, 0x80, 0x7F, 0xF8, 0x03, + 0xFF, 0x00, 0xFF, 0x00, 0x01, 0xFE, 0x01, 0xFF, + 0x01, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x07, 0xFC, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xBF, 0x40, 0xBF, + 0x47, 0xB8, 0x08, 0xF0, 0x08, 0xF7, 0x0E, 0xF1, + 0xFF, 0x00, 0xFF, 0x00, 0xC0, 0x7F, 0x80, 0x7F, + 0x80, 0x7F, 0x87, 0x7F, 0x87, 0x78, 0x80, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x40, 0x9F, 0x00, 0xFF, + 0x10, 0xEF, 0x90, 0x6F, 0x10, 0xEB, 0x14, 0xEB, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0xE0, 0x1F, 0xE0, + 0x0E, 0xF1, 0x0C, 0xF3, 0x0C, 0xF7, 0x18, 0xE7, + 0x00, 0xFF, 0x00, 0xFF, 0x20, 0x9F, 0x00, 0xFF, + 0x0C, 0xF3, 0x31, 0xC0, 0x60, 0x9F, 0x40, 0xBF, + 0xFF, 0x00, 0xFF, 0x00, 0xC0, 0x7F, 0x00, 0xFF, + 0x00, 0xFF, 0x1E, 0xEF, 0x3F, 0xC0, 0x7F, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x77, 0x04, 0xDB, + 0x00, 0xFB, 0x10, 0xEF, 0x00, 0xFD, 0x80, 0x77, + 0xFF, 0x00, 0xFF, 0x00, 0x78, 0x8F, 0x38, 0xE7, + 0x1C, 0xE7, 0x0C, 0xF3, 0x0E, 0xF3, 0x0E, 0xF9, + 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, 0x00, 0xFF, + 0x40, 0xB7, 0x00, 0xEF, 0x01, 0xDE, 0x02, 0xFC, + 0xFF, 0x00, 0xFF, 0x00, 0x7C, 0x83, 0x78, 0x87, + 0x38, 0xCF, 0x30, 0xDF, 0x21, 0xFE, 0x03, 0xFD, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xDF, 0x60, 0x9F, + 0xC0, 0x3F, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0xE0, 0x3F, 0xC0, + 0x7F, 0x80, 0xFF, 0x00, 0xFF, 0x80, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, 0x01, 0xFC, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0x01, 0xFC, 0x03, 0xFC, 0x07, 0xFE, 0x03, + 0x00, 0xFF, 0x40, 0x3F, 0x30, 0x8F, 0x00, 0xF7, + 0x80, 0x7F, 0x30, 0xCF, 0x01, 0xFE, 0x87, 0x78, + 0x01, 0xFE, 0x80, 0xFF, 0xE0, 0x5F, 0xF8, 0x0F, + 0x78, 0x87, 0x00, 0xFF, 0x00, 0xFF, 0x03, 0xFC, + 0x00, 0xFF, 0x08, 0xF7, 0x80, 0x6F, 0x80, 0x7F, + 0x80, 0x5F, 0x87, 0x78, 0x04, 0x7B, 0x08, 0x73, + 0xF8, 0x07, 0xF0, 0x0F, 0x70, 0x9F, 0x60, 0x9F, + 0x60, 0xBF, 0xC3, 0x3C, 0x87, 0xF8, 0x87, 0xFC, + 0xA0, 0x1F, 0x80, 0x7D, 0xE2, 0x1D, 0x02, 0xFD, + 0x02, 0xFD, 0xF0, 0x0F, 0x10, 0xEE, 0x11, 0xEE, + 0x43, 0xFC, 0xE3, 0x1E, 0x03, 0xFC, 0x01, 0xFE, + 0x01, 0xFE, 0xE1, 0x1E, 0xE1, 0x1F, 0xE1, 0x1E, + 0x44, 0xBB, 0x48, 0xB3, 0x48, 0xB7, 0x08, 0xF7, + 0x0A, 0xF5, 0x02, 0xF5, 0x80, 0x77, 0x90, 0x67, + 0x84, 0x7B, 0x84, 0x7F, 0x84, 0x7B, 0x84, 0x7B, + 0x8C, 0x73, 0x8C, 0x7B, 0x0E, 0xF9, 0x0E, 0xF9, + 0x86, 0x59, 0x06, 0xF9, 0x48, 0xB3, 0x08, 0xF7, + 0x10, 0xE7, 0x14, 0xEB, 0x24, 0xCB, 0x20, 0xDF, + 0x60, 0xBF, 0x44, 0xBB, 0x04, 0xFF, 0x0C, 0xF3, + 0x0C, 0xFB, 0x18, 0xE7, 0x18, 0xF7, 0x38, 0xC7, + 0x08, 0xD7, 0x48, 0x97, 0x48, 0xB7, 0x41, 0xBE, + 0x41, 0xBE, 0x01, 0xBE, 0x10, 0xAF, 0x90, 0x2F, + 0x30, 0xEF, 0x30, 0xEF, 0x30, 0xCF, 0x30, 0xCF, + 0x70, 0x8F, 0x70, 0xCF, 0x60, 0xDF, 0x60, 0xDF, + 0x04, 0xFB, 0x04, 0xFB, 0xFC, 0x03, 0x00, 0xFF, + 0x00, 0xFF, 0xF8, 0x02, 0x05, 0xFA, 0x05, 0xFA, + 0x03, 0xFC, 0x03, 0xFC, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x07, 0xFD, 0x02, 0xFD, 0x06, 0xF9, + 0x80, 0x7F, 0x80, 0x7F, 0x0F, 0xF0, 0x10, 0xE7, + 0x10, 0xEE, 0x1E, 0xE1, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x0E, 0xF1, 0x0F, 0xF8, + 0x0F, 0xF1, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x60, 0x8F, 0x00, 0xDF, 0x00, 0xFF, 0x00, 0xEF, + 0x04, 0xEB, 0x20, 0xCF, 0x22, 0xDD, 0xC1, 0x1E, + 0x38, 0xD7, 0x38, 0xE7, 0x18, 0xE7, 0x18, 0xF7, + 0x18, 0xF7, 0x1C, 0xF3, 0x3E, 0xC1, 0x7F, 0xA0, + 0x80, 0x3F, 0x80, 0x7F, 0x80, 0x7F, 0x01, 0xFE, + 0x00, 0xBD, 0x18, 0xE7, 0x00, 0xFF, 0x83, 0x7C, + 0x7F, 0xC0, 0x7F, 0x80, 0x7F, 0x80, 0x7E, 0x81, + 0x7E, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x81, 0x76, 0x80, 0x77, 0x10, 0xE7, 0x10, 0xEF, + 0x10, 0xEF, 0x21, 0xCE, 0x41, 0x9E, 0x81, 0x3E, + 0x0E, 0xF9, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF0, + 0x1F, 0xE0, 0x3E, 0xD1, 0x7E, 0xA1, 0xFE, 0x41, + 0x04, 0xF9, 0x08, 0xF3, 0x18, 0xE7, 0x10, 0xEF, + 0x10, 0xEF, 0x10, 0xEF, 0x00, 0xEF, 0x20, 0xCF, + 0x07, 0xFA, 0x07, 0xFC, 0x0F, 0xF0, 0x0F, 0xF0, + 0x0F, 0xF0, 0x1F, 0xE0, 0x1F, 0xF0, 0x1F, 0xF0, + 0x7C, 0x01, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x82, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x78, 0x87, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x0F, 0xF0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x70, 0x8E, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x01, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC3, 0x18, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x24, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x8F, 0x70, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xF8, 0x03, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x04, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x3E, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0xC1, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xE0, 0x1F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, +}; diff --git a/bsnes/gb/Core/graphics/sgb_animation_logo.inc b/bsnes/gb/Core/graphics/sgb_animation_logo.inc new file mode 100644 index 00000000..75075f49 --- /dev/null +++ b/bsnes/gb/Core/graphics/sgb_animation_logo.inc @@ -0,0 +1,563 @@ +static uint8_t animation_logo[] = { + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x3, 0x3, 0x3, 0x3, 0x3, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x1, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x1, 0x3, 0x3, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x1, 0x1, 0x1, 0x1, 0x3, 0x3, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x3, 0x3, 0x3, 0x3, 0x3, 0x1, 0x1, 0x0, 0x0, 0x0, 0x3, 0x3, 0x3, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x3, 0x3, 0x3, 0x3, 0x3, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x3, 0x3, 0x3, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, + 0x4, 0x4, 0x4, 0x3, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x3, 0x3, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xE, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, + 0x4, 0x4, 0x4, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x3, 0x3, 0x1, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xE, 0xE, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x4, 0x4, + 0x4, 0x4, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x3, 0x3, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0xE, 0xE, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, + 0x4, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x3, 0x3, 0x3, 0x1, 0x1, 0x0, 0x1, + 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x7, 0x0, 0x0, 0x1, 0x9, 0x9, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0xC, 0xC, 0xC, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0xE, 0xE, 0xE, 0xE, 0x1, + 0x0, 0x0, 0xE, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, + 0x4, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x3, 0x1, 0x1, 0x0, 0x0, 0x5, + 0x5, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x6, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x7, 0x7, 0x7, 0x9, 0x9, 0x9, 0x9, 0x9, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xC, 0xC, 0xC, 0xC, 0xC, 0xC, 0xC, 0x0, 0x0, 0x0, 0x0, 0xE, 0xE, 0xE, 0xE, 0x1, + 0x1, 0xE, 0xE, 0xE, 0xE, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x3, 0x3, 0x1, 0x0, 0x0, 0x5, 0x5, + 0x5, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x6, 0x6, 0x6, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x7, 0x7, 0x7, 0x9, 0x1, 0x1, 0x1, 0x9, 0x9, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC, + 0xC, 0xC, 0xC, 0xC, 0x1, 0x1, 0xC, 0xC, 0x0, 0x0, 0x0, 0xE, 0xE, 0xE, 0xE, 0xE, + 0xE, 0xE, 0xE, 0xE, 0xE, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x1, 0x1, 0x0, 0x1, 0x5, 0x5, + 0x5, 0x1, 0x0, 0x0, 0x0, 0x0, 0x6, 0x6, 0x6, 0x6, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, + 0x7, 0x7, 0x9, 0x1, 0x1, 0x0, 0x0, 0x9, 0x9, 0x1, 0x0, 0x0, 0x0, 0x0, 0xC, 0xC, + 0xC, 0xC, 0x1, 0x1, 0x0, 0xC, 0xC, 0xC, 0x1, 0x0, 0x0, 0xE, 0xE, 0xE, 0xE, 0xE, + 0xE, 0xE, 0xE, 0xE, 0xE, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x5, 0x5, 0x5, + 0x5, 0x1, 0x0, 0x0, 0x0, 0x1, 0x6, 0x6, 0x6, 0x1, 0x1, 0x0, 0x0, 0x0, 0x7, 0x7, + 0x7, 0x7, 0x1, 0x1, 0x0, 0x0, 0x1, 0x9, 0x9, 0x9, 0x0, 0x0, 0x0, 0x1, 0xC, 0xC, + 0xC, 0x1, 0x1, 0x0, 0x0, 0xC, 0xC, 0xC, 0x1, 0x0, 0x0, 0x1, 0xD, 0x1, 0x1, 0x1, + 0x1, 0xE, 0xE, 0xE, 0xE, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, + 0x4, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x5, 0x5, 0x5, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x6, 0x6, 0x6, 0x6, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0x7, + 0x7, 0x1, 0x1, 0x0, 0x0, 0x0, 0x9, 0x9, 0x9, 0x1, 0x0, 0x0, 0x0, 0xC, 0xC, 0xC, + 0xC, 0x1, 0x0, 0x0, 0xC, 0xC, 0xC, 0xC, 0x1, 0x0, 0x0, 0xD, 0xD, 0x1, 0x0, 0x0, + 0xE, 0xE, 0xF, 0xF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x4, + 0x4, 0x4, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x5, 0x5, 0x5, + 0x1, 0x0, 0x0, 0x0, 0x1, 0x6, 0x6, 0x6, 0x1, 0x1, 0x0, 0x0, 0x0, 0x7, 0x7, 0x7, + 0x7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x9, 0x9, 0x9, 0x1, 0x0, 0x0, 0xC, 0xC, 0xC, 0xC, + 0x1, 0x1, 0x0, 0x1, 0xC, 0xC, 0xC, 0x1, 0x1, 0x0, 0x1, 0xD, 0x1, 0x1, 0x0, 0xF, + 0xF, 0xF, 0xF, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, + 0x4, 0x4, 0x4, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x5, 0x5, 0x5, 0x1, + 0x1, 0x0, 0x0, 0x0, 0x6, 0x6, 0x6, 0x6, 0x1, 0x0, 0x0, 0x0, 0x1, 0x7, 0x7, 0x7, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x9, 0x9, 0x9, 0x1, 0x0, 0x0, 0xC, 0xC, 0xC, 0xC, + 0x1, 0x0, 0x1, 0xC, 0xC, 0xC, 0xC, 0x1, 0x0, 0x0, 0xD, 0xD, 0x1, 0x0, 0x1, 0xF, + 0xF, 0xF, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x4, 0x4, 0x4, 0x4, 0x4, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x6, 0x6, 0x1, 0x1, + 0x0, 0x0, 0x0, 0x6, 0x6, 0x6, 0x6, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0x7, 0x7, 0x7, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x9, 0x9, 0x9, 0x9, 0x1, 0x0, 0x1, 0xC, 0xC, 0xB, 0xB, + 0xC, 0xC, 0xC, 0xC, 0xC, 0xC, 0x1, 0x0, 0x0, 0xD, 0xD, 0x1, 0x1, 0x0, 0xF, 0xF, + 0xF, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x4, 0x4, 0x4, 0x4, 0x4, 0x0, 0x0, 0x0, 0x1, 0x6, 0x6, 0x6, 0x1, 0x0, + 0x0, 0x0, 0x1, 0x6, 0x6, 0x7, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x7, 0x7, 0x7, 0x1, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x9, 0x9, 0x9, 0x1, 0x1, 0x0, 0xC, 0xC, 0xB, 0xB, 0x1, + 0xC, 0xC, 0xC, 0xC, 0x1, 0x1, 0x1, 0x0, 0x1, 0xD, 0x1, 0x1, 0x0, 0x1, 0xF, 0xF, + 0xF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, 0x4, 0x4, 0x0, 0x0, 0x6, 0x6, 0x6, 0x1, 0x1, 0x0, + 0x0, 0x0, 0x6, 0x6, 0x7, 0x7, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0x7, 0x7, 0x8, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x9, 0x9, 0x9, 0x9, 0x1, 0x0, 0x0, 0xC, 0xB, 0xB, 0xB, 0x1, + 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0xD, 0xD, 0x1, 0x0, 0x0, 0xF, 0xF, 0xF, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5, 0x5, 0x5, 0x5, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x1, 0x4, 0x4, 0x4, 0x1, 0x0, 0x6, 0x6, 0x6, 0x1, 0x0, 0x0, + 0x0, 0x6, 0x6, 0x7, 0x7, 0x1, 0x1, 0x0, 0x0, 0x0, 0x7, 0x7, 0x7, 0x8, 0x8, 0x1, + 0x0, 0x0, 0x0, 0x1, 0x9, 0x9, 0xA, 0x1, 0x1, 0x0, 0xC, 0xB, 0xB, 0xB, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD, 0xD, 0x1, 0x1, 0x0, 0x0, 0xF, 0xF, 0xF, + 0x1, 0x0, 0x0, 0x0, 0x0, 0xF, 0xF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5, 0x5, 0x5, 0x5, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x4, 0x4, 0x1, 0x1, 0x6, 0x6, 0x6, 0x1, 0x0, 0x0, + 0x1, 0x6, 0x7, 0x7, 0x7, 0x1, 0x0, 0x0, 0x0, 0x7, 0x7, 0x7, 0x8, 0x8, 0xA, 0x1, + 0x0, 0x0, 0x0, 0x9, 0x9, 0xA, 0xA, 0x1, 0x0, 0x1, 0xB, 0xB, 0xB, 0xD, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xD, 0xD, 0x1, 0x1, 0x0, 0x0, 0x0, 0xF, 0xF, 0xF, + 0x1, 0x0, 0x0, 0x0, 0xF, 0xF, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5, 0x5, 0x5, 0x5, 0x5, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x4, 0x4, 0x1, 0x1, 0x6, 0x6, 0x1, 0x1, 0x0, 0x1, + 0x6, 0x1, 0x7, 0x7, 0x7, 0x1, 0x0, 0x0, 0x1, 0x7, 0x7, 0x8, 0x8, 0xA, 0xA, 0x1, + 0x0, 0x0, 0xA, 0xA, 0xA, 0xA, 0x1, 0x1, 0x0, 0xB, 0xB, 0x1, 0xD, 0xD, 0xD, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0xD, 0xD, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0xF, 0xF, 0xF, + 0x1, 0x0, 0x0, 0x0, 0xF, 0xF, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x5, 0x5, 0x5, 0x5, 0x5, + 0x5, 0x1, 0x0, 0x0, 0x1, 0x4, 0x4, 0x1, 0x1, 0x0, 0x6, 0x6, 0x1, 0x0, 0x1, 0x6, + 0x1, 0x1, 0x7, 0x7, 0x7, 0x1, 0x0, 0x1, 0x7, 0x7, 0x8, 0x8, 0x1, 0x1, 0xA, 0xA, + 0x1, 0xA, 0xA, 0xA, 0xA, 0x1, 0x1, 0xB, 0xB, 0xB, 0x1, 0x1, 0x1, 0xD, 0xD, 0x1, + 0x0, 0x0, 0x0, 0x1, 0xD, 0xD, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0xF, 0xF, + 0x1, 0x0, 0x1, 0xF, 0xF, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x5, 0x5, 0x5, 0x5, + 0x5, 0x5, 0x5, 0x4, 0x4, 0x4, 0x1, 0x1, 0x0, 0x0, 0x6, 0x6, 0x6, 0x6, 0x6, 0x1, + 0x1, 0x0, 0x1, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x8, 0x8, 0x8, 0x1, 0x0, 0x1, 0xA, + 0xA, 0xA, 0xA, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0x1, 0x1, 0x0, 0x0, 0xD, 0xD, 0xD, + 0xD, 0xD, 0xD, 0xD, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0xF, + 0xF, 0xF, 0xF, 0xF, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x5, + 0x5, 0x5, 0x5, 0x5, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x6, 0x1, 0x1, 0x1, + 0x0, 0x0, 0x0, 0x1, 0x7, 0x7, 0x7, 0x1, 0x8, 0x8, 0x8, 0x1, 0x1, 0x0, 0x0, 0x0, + 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0xD, + 0xD, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0xF, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x8, 0x8, 0x8, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x8, 0x8, 0x8, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x8, 0x8, 0x1, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x8, 0x8, 0x1, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, + 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x8, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, + 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x1, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, + 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, + 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x1, 0x0, 0x1, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, + 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, + 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, + 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, + 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x1, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, + 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, + 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, + 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, + 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, + 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, + 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, + 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, + 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, + 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x2, 0x2, 0x2, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, + 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, + 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 +}; +static const unsigned animation_logo_height = sizeof(animation_logo) / 160; diff --git a/bsnes/gb/Core/graphics/sgb_border.inc b/bsnes/gb/Core/graphics/sgb_border.inc new file mode 100644 index 00000000..d7d0a5c9 --- /dev/null +++ b/bsnes/gb/Core/graphics/sgb_border.inc @@ -0,0 +1,658 @@ +static const uint16_t palette[] = { + 0x0000, 0x0011, 0x18C6, 0x001A, 0x318C, 0x39CE, 0x5294, 0x5AD6, + 0x739C, 0x45A8, 0x4520, 0x18A5, 0x4631, 0x2033, 0x20EC, 0x18B7 +}; + +static const uint16_t tilemap[] = { + 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, + 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, + 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, + 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, + 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, + 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, + 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, + 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, 0x1002, + 0x1001, 0x1003, 0x1004, 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, + 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, + 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, + 0x1005, 0x1005, 0x1005, 0x1005, 0x1005, 0x5004, 0x5003, 0x1001, + 0x1001, 0x1006, 0x1007, 0x1007, 0x1007, 0x1008, 0x1009, 0x100A, + 0x100B, 0x100C, 0x100D, 0x100E, 0x100F, 0x1010, 0x1011, 0x1012, + 0x1013, 0x1014, 0x1015, 0x100E, 0x1016, 0x1017, 0x1018, 0x1019, + 0x101A, 0x101B, 0x101C, 0x1007, 0x1007, 0x1007, 0x5006, 0x1001, + 0x1001, 0x101D, 0x101E, 0x101E, 0x101E, 0x101F, 0x1020, 0x1021, + 0x1022, 0x1023, 0x1024, 0x1025, 0x5024, 0x1026, 0x1025, 0x1025, + 0x1027, 0x1028, 0x1029, 0x102A, 0x102B, 0x102C, 0x102D, 0x102E, + 0x102F, 0x1030, 0x1031, 0x101E, 0x101E, 0x101E, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1034, 0x1035, 0x5034, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x8034, 0x1036, 0xC034, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0xC01D, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0x1037, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1032, 0x1038, 0x1001, + 0x1001, 0x101D, 0x1032, 0x1032, 0x1032, 0x1033, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, 0x1000, + 0x1000, 0x1000, 0xC033, 0x1032, 0x1032, 0x1039, 0x103A, 0x1001, + 0x1001, 0x103B, 0x103C, 0x1032, 0x1032, 0xC03C, 0x103D, 0x103D, + 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, + 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, 0x103D, + 0x103D, 0x103D, 0x103E, 0x103F, 0x1040, 0x1041, 0x1001, 0x1001, + 0x1001, 0x1042, 0x1043, 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, + 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, + 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, 0x1044, + 0x1044, 0x1044, 0x1045, 0x1046, 0x1001, 0x1001, 0x1001, 0x1001, + 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1047, 0x1048, 0x1049, + 0x104A, 0x104B, 0x104C, 0x104D, 0x104E, 0x104F, 0x1050, 0x1051, + 0x1052, 0x1053, 0x1054, 0x1055, 0x1056, 0x1057, 0x1058, 0x1059, + 0x105A, 0x105B, 0x105C, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, + 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x105D, 0x105E, 0x105F, + 0x1060, 0x1061, 0x1062, 0x1063, 0x1064, 0x1065, 0x1066, 0x1067, + 0x1068, 0x1069, 0x106A, 0x106B, 0x106C, 0x106D, 0x106E, 0x106F, + 0x1070, 0x1071, 0x1072, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, + 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, 0x1073, 0x1074, 0x1075, + 0x1076, 0x1077, 0x1078, 0x1079, 0x107A, 0x107B, 0x107C, 0x107D, + 0x107E, 0x107F, 0x1080, 0x1081, 0x1082, 0x1083, 0x507A, 0x1084, + 0x1001, 0x1085, 0x507A, 0x1001, 0x1001, 0x1001, 0x1001, 0x1001, +}; + +const uint8_t tiles[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0x01, 0xFE, 0x06, 0xF9, 0x08, 0xF7, + 0x11, 0xEF, 0x22, 0xDB, 0x20, 0xDB, 0x40, 0xB7, + 0xFF, 0x00, 0xFF, 0x00, 0xFE, 0x00, 0xF8, 0x00, + 0xF1, 0x00, 0xE6, 0x04, 0xE4, 0x00, 0xC8, 0x00, + 0x7F, 0x80, 0x80, 0x7F, 0x00, 0xFF, 0x7F, 0xFF, + 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x80, 0x00, 0x00, 0x00, 0x7F, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0xB7, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0xC8, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x03, 0xFF, 0x02, 0xFF, + 0x02, 0xFF, 0x02, 0xFF, 0x02, 0xFF, 0x02, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x00, + 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xC1, 0xDD, 0x00, 0xC9, + 0x14, 0xFF, 0x14, 0xFF, 0x14, 0xFF, 0x00, 0xC9, + 0x00, 0x00, 0x00, 0x00, 0xE3, 0x00, 0x36, 0x22, + 0x14, 0x00, 0x14, 0x00, 0x14, 0x00, 0x36, 0x22, + 0x00, 0xFF, 0x00, 0xFF, 0xC7, 0xDF, 0x01, 0xCF, + 0x11, 0xFF, 0x11, 0xFF, 0x11, 0xFF, 0x01, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0xE7, 0x00, 0x31, 0x20, + 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x31, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0xC2, 0xFF, 0x03, 0xFF, + 0x02, 0xFE, 0x02, 0xFE, 0x02, 0xFF, 0x02, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xC2, 0x00, 0x03, 0x00, + 0x03, 0x01, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xFF, 0x18, 0xFF, + 0x08, 0x4E, 0x08, 0x4E, 0x09, 0x1F, 0x08, 0x1C, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x18, 0x00, + 0xB9, 0x10, 0xB9, 0xA1, 0xE9, 0xA0, 0xEB, 0x41, + 0x00, 0xFF, 0x00, 0xFF, 0x4F, 0xFF, 0x02, 0x1F, + 0x02, 0x4F, 0x02, 0x4F, 0xF2, 0xFF, 0x02, 0xE7, + 0x00, 0x00, 0x00, 0x00, 0x4F, 0x00, 0xE2, 0xA0, + 0xB2, 0xA0, 0xB2, 0x10, 0xF2, 0x00, 0x1A, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0xBC, 0xFD, 0x22, 0xFB, + 0x22, 0xFB, 0x3C, 0xFD, 0x24, 0xFF, 0x26, 0xF9, + 0x00, 0x00, 0x00, 0x00, 0xBE, 0x00, 0x26, 0x00, + 0x26, 0x00, 0x3E, 0x00, 0x24, 0x00, 0x26, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x50, 0xFF, 0x49, 0xEF, + 0x49, 0xF0, 0x46, 0xFF, 0x49, 0xF0, 0x49, 0xEF, + 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x59, 0x00, + 0x4F, 0x06, 0x46, 0x00, 0x4F, 0x06, 0x59, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x88, 0xFF, 0x00, 0x72, + 0x00, 0xF2, 0x05, 0xFF, 0x00, 0xF8, 0x00, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x88, 0x00, 0x8D, 0x08, + 0x0D, 0x05, 0x05, 0x00, 0x07, 0x05, 0x87, 0x02, + 0x00, 0xFF, 0x00, 0xFF, 0x8A, 0xFF, 0x02, 0x27, + 0x02, 0x27, 0x52, 0xFF, 0x02, 0x8F, 0x02, 0x8F, + 0x00, 0x00, 0x00, 0x00, 0x8A, 0x00, 0xDA, 0x88, + 0xDA, 0x50, 0x52, 0x00, 0x72, 0x50, 0x72, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0xFA, 0xFF, 0x22, 0xFF, + 0x22, 0xFF, 0x23, 0xFF, 0x22, 0xFF, 0x22, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xFA, 0x00, 0x22, 0x00, + 0x22, 0x00, 0x23, 0x00, 0x22, 0x00, 0x22, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x20, 0xFF, 0x20, 0xFF, + 0x20, 0xFF, 0xE0, 0xFF, 0x20, 0xFF, 0x20, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, + 0x20, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x20, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x33, 0x37, 0x00, 0x77, + 0x80, 0xFF, 0x20, 0x27, 0x08, 0xFF, 0x00, 0x77, + 0x00, 0x00, 0x00, 0x00, 0xFB, 0x40, 0x88, 0x88, + 0x80, 0x00, 0xF8, 0x50, 0x08, 0x00, 0x88, 0x88, + 0x00, 0xFF, 0x00, 0xFF, 0xEF, 0xFF, 0x88, 0xFF, + 0x88, 0xFF, 0x8F, 0xFF, 0x88, 0xFF, 0x88, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0xEF, 0x00, 0x88, 0x00, + 0x88, 0x00, 0x8F, 0x00, 0x88, 0x00, 0x88, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0xF9, 0xFD, 0x80, 0xF9, + 0x84, 0xFF, 0xF4, 0xFF, 0x84, 0xFF, 0x80, 0xF9, + 0x00, 0x00, 0x00, 0x00, 0xFB, 0x00, 0x86, 0x02, + 0x84, 0x00, 0xF4, 0x00, 0x84, 0x00, 0x86, 0x02, + 0x00, 0xFF, 0x00, 0xFF, 0xC0, 0xDF, 0x00, 0xCF, + 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x00, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x30, 0x20, + 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x30, 0x20, + 0x00, 0xFF, 0x00, 0xFF, 0x30, 0x36, 0x00, 0x74, + 0x82, 0xFF, 0x22, 0x27, 0x0A, 0xFF, 0x00, 0x74, + 0x00, 0x00, 0x00, 0x00, 0xF9, 0x40, 0x8B, 0x89, + 0x82, 0x00, 0xFA, 0x50, 0x0A, 0x00, 0x8B, 0x89, + 0x00, 0xFF, 0x00, 0xFF, 0xE2, 0xEF, 0x02, 0xE7, + 0x0A, 0xFF, 0x0A, 0xFF, 0x0A, 0xFF, 0x00, 0xE4, + 0x00, 0x00, 0x00, 0x00, 0xF2, 0x00, 0x1A, 0x10, + 0x0A, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x1B, 0x12, + 0x00, 0xFF, 0x00, 0xFF, 0x14, 0xFF, 0x16, 0xFF, + 0x14, 0xFC, 0x15, 0xFE, 0x14, 0xFF, 0x04, 0xCF, + 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x16, 0x00, + 0x17, 0x01, 0x15, 0x00, 0x14, 0x00, 0x34, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0x2F, 0xFF, 0x28, 0xFF, + 0x28, 0xFF, 0xA8, 0x7F, 0x28, 0x3F, 0x68, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x28, 0x00, + 0x28, 0x00, 0xA8, 0x00, 0xE8, 0x80, 0x68, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0x3F, + 0x40, 0xFF, 0x40, 0xFF, 0x40, 0xFF, 0x00, 0x3F, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x80, + 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xC0, 0x80, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xC1, 0xDD, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC1, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x02, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x4A, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x0A, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x22, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x82, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x20, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x60, 0x67, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF8, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x8F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x8F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xA2, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xF9, 0xFD, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC0, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x60, 0x66, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF9, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xE0, 0xEE, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xF1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xC4, 0xDF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xE4, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x2F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0xFF, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, + 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0x01, 0xFF, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x3C, 0xFF, + 0x7E, 0xFF, 0xE7, 0xE7, 0xFF, 0x7E, 0xFF, 0x7E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x7E, + 0x81, 0xC3, 0x00, 0x99, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0x7E, 0xFF, 0x3C, 0xFF, 0x00, 0x7E, 0x81, + 0x3C, 0xC3, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0xC3, 0x81, + 0x7E, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF7, 0x00, 0xF7, 0x00, 0xF7, 0x00, 0xF7, + 0x00, 0xF7, 0x00, 0xED, 0x00, 0xED, 0x00, 0xED, + 0x09, 0x00, 0x09, 0x00, 0x09, 0x00, 0x09, 0x00, + 0x09, 0x00, 0x11, 0x02, 0x11, 0x02, 0x11, 0x02, + 0x00, 0xED, 0x00, 0xDB, 0x00, 0xDB, 0x00, 0xDB, + 0x00, 0xB7, 0x00, 0xB7, 0x00, 0x6F, 0x00, 0x6F, + 0x11, 0x02, 0x23, 0x04, 0x23, 0x04, 0x23, 0x04, + 0x47, 0x08, 0x47, 0x08, 0x8F, 0x10, 0x8F, 0x10, + 0x00, 0xFE, 0x00, 0xFD, 0x00, 0xFB, 0x00, 0xF7, + 0x00, 0xEE, 0x00, 0xDD, 0x00, 0xBB, 0x00, 0x77, + 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x08, 0x00, + 0x10, 0x01, 0x21, 0x02, 0x43, 0x04, 0x87, 0x08, + 0x00, 0xDF, 0x00, 0xBF, 0x00, 0xBF, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x1F, 0x20, 0x3F, 0x40, 0x3F, 0x40, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xEF, 0x00, 0xB7, + 0x00, 0xB7, 0x00, 0xDB, 0x00, 0xDD, 0x00, 0xEE, + 0x90, 0x00, 0x90, 0x00, 0x90, 0x00, 0x88, 0x40, + 0x88, 0x40, 0xC4, 0x20, 0xC2, 0x20, 0xE1, 0x10, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFC, + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFC, 0x00, 0xE3, 0x00, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x00, 0x1C, 0x00, 0xE0, 0x00, + 0x00, 0xFE, 0x00, 0xFD, 0x00, 0xF3, 0x00, 0xEF, + 0x00, 0x1C, 0x00, 0xF3, 0x00, 0xEF, 0x00, 0x1F, + 0x01, 0x00, 0x02, 0x00, 0x0C, 0x00, 0x10, 0x00, + 0xE0, 0x03, 0x03, 0x0C, 0x0F, 0x10, 0x1F, 0xE0, + 0x00, 0xEF, 0x00, 0xDF, 0x00, 0xBF, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x0F, 0x10, 0x1F, 0x20, 0x3F, 0x40, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xF7, 0x00, 0xF9, 0x00, 0xFE, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xF0, 0x08, 0xF8, 0x06, 0xFE, 0x01, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x80, 0x00, 0xFF, 0x00, 0x7F, 0x00, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x7F, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x7F, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0x03, 0x00, 0xFF, 0x00, 0xFC, 0x00, 0x03, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0xFC, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFC, 0x00, 0xE3, 0x00, 0x1F, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x03, 0x03, 0x1C, 0x1F, 0xE0, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x01, 0xFF, 0x01, 0xFD, 0x03, 0xFF, 0x03, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x01, 0xFE, 0x02, 0xFE, 0x02, 0xFC, 0x00, + 0x0E, 0xEE, 0x3F, 0xFF, 0x75, 0x71, 0xFB, 0xE7, + 0xE3, 0xCB, 0xC7, 0x9F, 0x07, 0x3E, 0x84, 0x7C, + 0xFB, 0x1B, 0xE6, 0x26, 0x8E, 0x82, 0x3E, 0x22, + 0x7C, 0x54, 0x7D, 0x25, 0xF9, 0x40, 0xFB, 0x01, + 0x00, 0xFF, 0x00, 0xFF, 0x80, 0xFF, 0x80, 0xFF, + 0x00, 0x7F, 0x80, 0x4F, 0x31, 0x7F, 0x71, 0xFD, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0xFF, 0x80, + 0xFF, 0x00, 0xFF, 0x30, 0xFF, 0xB1, 0xDE, 0x52, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0x7B, 0x87, 0xFF, 0x8E, 0xFE, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x84, 0xFA, 0x82, 0xF9, 0x88, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0xC1, 0xFD, 0xE3, 0x7B, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xC3, 0xBC, 0x24, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x01, 0xFF, 0x03, 0xFF, 0xE3, 0xFB, 0xF7, 0xBF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x01, 0xFE, 0x02, 0x7C, 0x64, 0xFC, 0xB4, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x7F, 0x80, 0xFF, 0xA0, 0x2F, 0xF0, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x80, 0xFF, 0x80, 0xFF, 0x70, 0x8F, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x02, 0xFD, 0x00, 0xF7, + 0x00, 0xFF, 0x11, 0xEE, 0x11, 0xEE, 0x10, 0xEF, + 0xFF, 0x00, 0xFF, 0x00, 0xFC, 0x03, 0xF8, 0x0F, + 0xF0, 0x0F, 0xE0, 0x1F, 0xE1, 0x1E, 0xE0, 0x1F, + 0x00, 0xFF, 0x00, 0xFF, 0x10, 0xE7, 0x00, 0xFB, + 0xC4, 0x3B, 0x98, 0x03, 0x00, 0xEF, 0x80, 0x7F, + 0xFF, 0x00, 0xFF, 0x00, 0x0F, 0xF8, 0x07, 0xFC, + 0x07, 0xF8, 0xEF, 0x74, 0xFF, 0x10, 0x7F, 0x80, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xEF, 0x00, 0xFF, 0x22, 0xDD, 0x06, 0xB9, + 0xFF, 0x00, 0xFF, 0x00, 0xF8, 0x07, 0xF0, 0x0F, + 0xF0, 0x1F, 0xE0, 0x1F, 0xC0, 0x3F, 0xC4, 0x7B, + 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7D, 0x02, 0xFD, + 0x02, 0xBD, 0x40, 0xBF, 0x40, 0xBF, 0x40, 0xBF, + 0xFF, 0x00, 0xFF, 0x00, 0x7E, 0x83, 0x7C, 0x83, + 0x7C, 0xC3, 0x7C, 0x83, 0x3C, 0xC3, 0x3C, 0xC3, + 0x00, 0xFF, 0x00, 0xFF, 0x10, 0xEF, 0x00, 0xFF, + 0x00, 0xF7, 0x00, 0xF7, 0x48, 0xB6, 0x48, 0xB7, + 0xFF, 0x00, 0xFF, 0x00, 0x0F, 0xF0, 0x0F, 0xF0, + 0x0F, 0xF8, 0x0F, 0xF8, 0x07, 0xF9, 0x06, 0xF9, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xBF, 0x02, 0xFC, + 0x02, 0x7D, 0x02, 0xFD, 0x02, 0xFD, 0x20, 0xDD, + 0xFF, 0x00, 0xFF, 0x00, 0xC1, 0x7E, 0x81, 0x7F, + 0x81, 0xFE, 0x01, 0xFE, 0x03, 0xFC, 0x03, 0xFE, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xBF, 0x40, 0xBF, + 0x47, 0xB8, 0x08, 0xF0, 0x08, 0xF7, 0x0F, 0xF0, + 0xFF, 0x00, 0xFF, 0x00, 0xC0, 0x7F, 0x80, 0x7F, + 0x80, 0x7F, 0x87, 0x7F, 0x87, 0x78, 0x80, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, 0x24, 0xCB, + 0xE4, 0x1B, 0x00, 0x1F, 0x00, 0xFF, 0x80, 0x3F, + 0xFF, 0x00, 0xFF, 0x00, 0x1C, 0xE7, 0x18, 0xF7, + 0x18, 0xE7, 0xF8, 0xE7, 0xF8, 0x07, 0x78, 0xC7, + 0x00, 0xFF, 0x00, 0xFF, 0x04, 0xF9, 0x00, 0xFF, + 0x71, 0x8E, 0x89, 0x06, 0x81, 0x7E, 0xE1, 0x1E, + 0xFF, 0x00, 0xFF, 0x00, 0x03, 0xFE, 0x01, 0xFE, + 0x00, 0xFF, 0x70, 0xFF, 0x70, 0x8F, 0x01, 0xFE, + 0x00, 0xFF, 0x00, 0xFF, 0x02, 0xF9, 0x00, 0xFF, + 0x00, 0xFF, 0x03, 0xFC, 0x06, 0xB9, 0x44, 0xBB, + 0xFF, 0x00, 0xFF, 0x00, 0xFC, 0x07, 0xF0, 0x0F, + 0xE0, 0x1F, 0xC1, 0x3E, 0xC3, 0x7C, 0x87, 0x78, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xF7, 0x00, 0xFD, + 0xC0, 0x3F, 0x11, 0x0E, 0x00, 0xFF, 0x08, 0xF7, + 0xFF, 0x00, 0xFF, 0x00, 0x07, 0xF8, 0x03, 0xFE, + 0x01, 0xFE, 0xE0, 0xFF, 0xF0, 0x0F, 0xF0, 0x0F, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0x77, 0x40, 0xBF, + 0x04, 0xBB, 0x00, 0xFE, 0x00, 0xDD, 0x00, 0x7F, + 0xFF, 0x00, 0xFF, 0x00, 0x87, 0xF8, 0x87, 0x78, + 0xC3, 0x7C, 0xC3, 0x3D, 0xE2, 0x3F, 0xE0, 0x9F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFD, 0x06, 0xF9, + 0x0C, 0x73, 0x08, 0xF7, 0x10, 0xE7, 0x20, 0xCF, + 0xFF, 0x00, 0xFF, 0x00, 0xC3, 0x3E, 0x83, 0x7C, + 0x87, 0xF8, 0x0F, 0xF0, 0x1F, 0xE8, 0x3F, 0xD0, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xF7, 0x00, 0xFF, + 0x01, 0xDE, 0x06, 0xF8, 0x1C, 0xC3, 0x00, 0xF3, + 0xFF, 0x00, 0xFF, 0x00, 0xF8, 0x0F, 0xE0, 0x1F, + 0xE0, 0x3F, 0xC3, 0x3D, 0xE7, 0x38, 0xFF, 0x0C, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xDF, 0x00, 0xFF, + 0x00, 0xF7, 0x08, 0x77, 0x08, 0xF7, 0x08, 0xF7, + 0xFF, 0x00, 0xFF, 0x00, 0x3F, 0xE0, 0x0F, 0xF0, + 0x0F, 0xF8, 0x87, 0xF8, 0x87, 0x78, 0x07, 0xF8, + 0x03, 0xFF, 0x03, 0xFF, 0x01, 0xFF, 0x00, 0xFE, + 0x18, 0xDF, 0x1C, 0xFD, 0x0F, 0xEF, 0x07, 0xF7, + 0xFC, 0x00, 0xFE, 0x02, 0xFF, 0x01, 0xFF, 0x01, + 0xFF, 0x38, 0xF3, 0x12, 0xF9, 0x19, 0xFC, 0x0C, + 0x02, 0x79, 0x80, 0xFD, 0xC0, 0xDF, 0xF0, 0xFE, + 0x79, 0x3F, 0x19, 0xDB, 0x19, 0xFB, 0xF9, 0xF7, + 0xFF, 0x84, 0xFF, 0x82, 0x7F, 0x60, 0x9F, 0x91, + 0xEF, 0xA9, 0xF6, 0x34, 0xFE, 0x1C, 0x1F, 0x11, + 0x63, 0xEF, 0xF3, 0xEB, 0xC6, 0xCE, 0xEF, 0xDE, + 0x8C, 0x9C, 0xDE, 0xBD, 0x9C, 0x9D, 0xFF, 0xEF, + 0x9E, 0x02, 0xBC, 0xA4, 0x3D, 0x14, 0x7B, 0x4A, + 0x73, 0x21, 0xF7, 0x94, 0xF7, 0xF6, 0xFE, 0xEE, + 0x8D, 0xEC, 0x9E, 0x7D, 0x1C, 0x5B, 0x38, 0xFA, + 0x79, 0xF7, 0x71, 0x75, 0xF3, 0xF3, 0xEF, 0xCF, + 0xF3, 0x90, 0xF7, 0x14, 0xEF, 0xA8, 0xEF, 0x2D, + 0xCF, 0x41, 0x8E, 0x8A, 0x3C, 0x3C, 0x39, 0x19, + 0x67, 0xFF, 0xEF, 0xFE, 0xEC, 0xDC, 0xCF, 0xCF, + 0xDD, 0xDC, 0xDC, 0x9F, 0x2C, 0x2F, 0xD7, 0xC7, + 0xB9, 0x21, 0xBB, 0xAA, 0xB3, 0x81, 0x76, 0x76, + 0x77, 0x76, 0xE7, 0xA4, 0xD7, 0x44, 0xFB, 0xCB, + 0xB3, 0x37, 0x73, 0x72, 0xF4, 0xEC, 0xEF, 0xCD, + 0xCD, 0x09, 0x11, 0xF3, 0x29, 0xA7, 0xF1, 0xCF, + 0xCD, 0x49, 0xDF, 0xDE, 0xBF, 0xA5, 0x7F, 0x5D, + 0xF6, 0x32, 0xFE, 0x14, 0xFE, 0x70, 0xFF, 0xC1, + 0xF0, 0x77, 0xF0, 0x67, 0xE0, 0xCF, 0x80, 0x97, + 0xC8, 0xBB, 0x98, 0xBB, 0x90, 0xD3, 0xE8, 0xE7, + 0xDF, 0x58, 0xBF, 0x28, 0x7F, 0x50, 0x7F, 0x28, + 0xF7, 0x84, 0xFF, 0xDC, 0xEF, 0xA4, 0xDF, 0xC0, + 0x00, 0xFF, 0x04, 0xF3, 0x03, 0xF8, 0x00, 0xFF, + 0x08, 0xF7, 0x03, 0xFC, 0x00, 0xBF, 0x18, 0xC7, + 0xF0, 0x0F, 0xF8, 0x0F, 0xFE, 0x05, 0xFF, 0x00, + 0xE7, 0x18, 0xC0, 0x3F, 0xC0, 0x7F, 0xE0, 0x3F, + 0x00, 0xFF, 0x00, 0xFF, 0x08, 0xF6, 0x08, 0x77, + 0x08, 0xF5, 0x08, 0xF7, 0x10, 0xE7, 0x70, 0x87, + 0x1F, 0xE0, 0x0F, 0xF0, 0x07, 0xF9, 0x86, 0xF9, + 0x86, 0x7B, 0x0C, 0xF3, 0x08, 0xFF, 0x38, 0xCF, + 0x0A, 0xF1, 0x88, 0x77, 0x0E, 0xF1, 0x00, 0xFF, + 0x00, 0xFF, 0x7F, 0x80, 0x41, 0xBE, 0x81, 0x3E, + 0x84, 0x7F, 0x0E, 0xF1, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x3E, 0xC1, 0x7E, 0x81, 0x7E, 0xC1, + 0x04, 0xFB, 0x04, 0xDB, 0x24, 0xDB, 0x20, 0xDF, + 0x20, 0xDF, 0x00, 0xFF, 0x08, 0xE7, 0x19, 0xE6, + 0x38, 0xC7, 0x38, 0xE7, 0x38, 0xC7, 0x18, 0xE7, + 0x18, 0xE7, 0x18, 0xE7, 0x10, 0xFF, 0x10, 0xEF, + 0x48, 0xB5, 0x80, 0x3F, 0x84, 0x7B, 0x80, 0x7F, + 0xA1, 0x5E, 0x21, 0x5E, 0x02, 0x7C, 0x02, 0x7D, + 0x46, 0xBB, 0x44, 0xFB, 0x40, 0xBF, 0x40, 0xBF, + 0xC0, 0x3F, 0xC1, 0xBE, 0xE1, 0x9F, 0xE3, 0x9C, + 0x60, 0x9D, 0x64, 0x99, 0x84, 0x3B, 0x84, 0x7B, + 0x04, 0x7B, 0x40, 0xBB, 0x41, 0xBA, 0x09, 0xF2, + 0x03, 0xFE, 0x43, 0xBE, 0x43, 0xFC, 0xC3, 0x3C, + 0xC7, 0xB8, 0x87, 0x7C, 0x86, 0x7D, 0x86, 0x7D, + 0x80, 0x7F, 0x80, 0x7F, 0x8F, 0x70, 0x10, 0xEF, + 0x10, 0xEF, 0x1F, 0xE0, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x0F, 0xF0, 0x0F, 0xF0, + 0x0F, 0xF0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x48, 0xB7, 0x48, 0xB7, 0xC0, 0x3F, 0x01, 0xFE, + 0x01, 0xFE, 0x81, 0x2E, 0x50, 0xAF, 0x50, 0xAF, + 0x30, 0xCF, 0x30, 0xCF, 0xF0, 0x0F, 0xF0, 0x0F, + 0xF0, 0x0F, 0x70, 0xDF, 0x20, 0xDF, 0x60, 0x9F, + 0x06, 0xF8, 0x00, 0xFD, 0xF0, 0x0F, 0x00, 0x7E, + 0x00, 0xEE, 0xE2, 0x1C, 0x02, 0xFD, 0x0C, 0xF1, + 0x03, 0xFD, 0x03, 0xFE, 0xE1, 0x1E, 0xF1, 0x8F, + 0xF1, 0x1F, 0x01, 0xFF, 0x03, 0xFC, 0x07, 0xFA, + 0x08, 0xF3, 0x08, 0xF7, 0x08, 0xF7, 0x00, 0xFF, + 0x40, 0xBB, 0x01, 0xFE, 0x20, 0xDF, 0x18, 0xE7, + 0x87, 0x7C, 0x87, 0x78, 0x87, 0x78, 0x87, 0x78, + 0x87, 0x7C, 0xC0, 0x3F, 0xE0, 0x1F, 0xF0, 0x0F, + 0x08, 0xF7, 0x08, 0xF7, 0x01, 0xFE, 0x11, 0xEE, + 0x01, 0xDE, 0x82, 0x7C, 0x04, 0xF9, 0x38, 0xC3, + 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0, 0x1F, + 0xE1, 0x3E, 0x03, 0xFD, 0x07, 0xFA, 0x0F, 0xF4, + 0x10, 0x6F, 0x00, 0x7F, 0x01, 0x7E, 0x01, 0xFE, + 0x01, 0xFE, 0x11, 0xEE, 0x10, 0xEE, 0x12, 0xEC, + 0xE0, 0x9F, 0xF0, 0x8F, 0xF0, 0x8F, 0xF0, 0x0F, + 0xF0, 0x0F, 0xE1, 0x1E, 0xE1, 0x1F, 0xE1, 0x1F, + 0x40, 0x9F, 0x80, 0x3F, 0x80, 0x7F, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x7F, 0xA0, 0x7F, 0xC0, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFB, 0x08, 0xF7, 0x00, 0xFF, + 0x01, 0xBE, 0x03, 0xFC, 0x00, 0x7F, 0x80, 0x7F, + 0xFE, 0x01, 0xFC, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, + 0xC1, 0x7E, 0x80, 0x7F, 0x80, 0xFF, 0x00, 0xFF, + 0x08, 0xF7, 0x10, 0xE7, 0x60, 0x8F, 0xC0, 0x3F, + 0x80, 0x7F, 0xE0, 0x0F, 0x00, 0xEF, 0x00, 0xEF, + 0x0F, 0xF0, 0x1F, 0xE8, 0x3F, 0xD0, 0x7F, 0x80, + 0xFF, 0x00, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, + 0x02, 0xF8, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x04, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xD0, 0xC6, 0x00, 0x1F, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0xC9, 0xFF, 0xE0, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xE7, 0x86, 0x01, 0x39, 0x01, 0xFF, 0x03, 0xFF, + 0x03, 0xFF, 0x00, 0xFC, 0x00, 0xFE, 0x00, 0xFF, + 0xFF, 0x9E, 0xFF, 0xC7, 0xFE, 0x00, 0xFE, 0x02, + 0xFF, 0x03, 0xFF, 0x02, 0xFF, 0x01, 0xFF, 0x00, + 0xC3, 0xD3, 0xC0, 0xBC, 0x80, 0xBF, 0x00, 0x7F, + 0x80, 0x7F, 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, + 0x7F, 0x6B, 0x7F, 0x03, 0xFF, 0xC0, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x00, + 0xC7, 0x1B, 0x00, 0x7C, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x23, 0xFF, 0x83, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC0, 0x1F, 0x00, 0x7F, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x20, 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x50, 0x4F, 0x00, 0x9F, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x40, 0xFF, 0x60, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x07, 0xF0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x08, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xC7, 0x18, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x20, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x80, 0x7F, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xF7, 0x08, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x0C, 0xE1, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x12, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x38, 0x87, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x40, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x8F, 0x30, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x40, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xF0, 0x07, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x08, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x03, 0xF0, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x0C, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x0E, 0xF1, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0x7F, 0x80, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00 +}; diff --git a/bsnes/gb/Core/joypad.c b/bsnes/gb/Core/joypad.c new file mode 100644 index 00000000..b8d4fdb4 --- /dev/null +++ b/bsnes/gb/Core/joypad.c @@ -0,0 +1,92 @@ +#include "gb.h" +#include + +void GB_update_joyp(GB_gameboy_t *gb) +{ + if (gb->model & GB_MODEL_NO_SFC_BIT) return; + + uint8_t key_selection = 0; + uint8_t previous_state = 0; + + /* Todo: add delay to key selection */ + previous_state = gb->io_registers[GB_IO_JOYP] & 0xF; + key_selection = (gb->io_registers[GB_IO_JOYP] >> 4) & 3; + gb->io_registers[GB_IO_JOYP] &= 0xF0; + uint8_t current_player = gb->sgb? (gb->sgb->current_player & (gb->sgb->player_count - 1) & 3) : 0; + switch (key_selection) { + case 3: + if (gb->sgb && gb->sgb->player_count > 1) { + gb->io_registers[GB_IO_JOYP] |= 0xF - current_player; + } + else { + /* Nothing is wired, all up */ + gb->io_registers[GB_IO_JOYP] |= 0x0F; + } + break; + + case 2: + /* Direction keys */ + for (uint8_t i = 0; i < 4; i++) { + gb->io_registers[GB_IO_JOYP] |= (!gb->keys[current_player][i]) << i; + } + /* Forbid pressing two opposing keys, this breaks a lot of games; even if it's somewhat possible. */ + if (!(gb->io_registers[GB_IO_JOYP] & 1)) { + gb->io_registers[GB_IO_JOYP] |= 2; + } + if (!(gb->io_registers[GB_IO_JOYP] & 4)) { + gb->io_registers[GB_IO_JOYP] |= 8; + } + break; + + case 1: + /* Other keys */ + for (uint8_t i = 0; i < 4; i++) { + gb->io_registers[GB_IO_JOYP] |= (!gb->keys[current_player][i + 4]) << i; + } + break; + + case 0: + for (uint8_t i = 0; i < 4; i++) { + gb->io_registers[GB_IO_JOYP] |= (!(gb->keys[current_player][i] || gb->keys[current_player][i + 4])) << i; + } + break; + + default: + break; + } + + /* Todo: This assumes the keys *always* bounce, which is incorrect when emulating an SGB */ + if (previous_state != (gb->io_registers[GB_IO_JOYP] & 0xF)) { + /* The joypad interrupt DOES occur on CGB (Tested on CGB-E), unlike what some documents say. */ + gb->io_registers[GB_IO_IF] |= 0x10; + } + + gb->io_registers[GB_IO_JOYP] |= 0xC0; +} + +void GB_icd_set_joyp(GB_gameboy_t *gb, uint8_t value) +{ + uint8_t previous_state = gb->io_registers[GB_IO_JOYP] & 0xF; + gb->io_registers[GB_IO_JOYP] &= 0xF0; + gb->io_registers[GB_IO_JOYP] |= value & 0xF; + + if (previous_state & ~(gb->io_registers[GB_IO_JOYP] & 0xF)) { + gb->io_registers[GB_IO_IF] |= 0x10; + } + gb->io_registers[GB_IO_JOYP] |= 0xC0; +} + +void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed) +{ + assert(index >= 0 && index < GB_KEY_MAX); + gb->keys[0][index] = pressed; + GB_update_joyp(gb); +} + +void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned player, bool pressed) +{ + assert(index >= 0 && index < GB_KEY_MAX); + assert(player < 4); + gb->keys[player][index] = pressed; + GB_update_joyp(gb); +} diff --git a/bsnes/gb/Core/joypad.h b/bsnes/gb/Core/joypad.h new file mode 100644 index 00000000..21fad534 --- /dev/null +++ b/bsnes/gb/Core/joypad.h @@ -0,0 +1,25 @@ +#ifndef joypad_h +#define joypad_h +#include "gb_struct_def.h" +#include + +typedef enum { + GB_KEY_RIGHT, + GB_KEY_LEFT, + GB_KEY_UP, + GB_KEY_DOWN, + GB_KEY_A, + GB_KEY_B, + GB_KEY_SELECT, + GB_KEY_START, + GB_KEY_MAX +} GB_key_t; + +void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed); +void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned player, bool pressed); +void GB_icd_set_joyp(GB_gameboy_t *gb, uint8_t value); + +#ifdef GB_INTERNAL +void GB_update_joyp(GB_gameboy_t *gb); +#endif +#endif /* joypad_h */ diff --git a/bsnes/gb/Core/mbc.c b/bsnes/gb/Core/mbc.c new file mode 100644 index 00000000..22596817 --- /dev/null +++ b/bsnes/gb/Core/mbc.c @@ -0,0 +1,167 @@ +#include +#include +#include +#include "gb.h" + +const GB_cartridge_t GB_cart_defs[256] = { + // From http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header#0147_-_Cartridge_Type + /* MBC SUBTYPE RAM BAT. RTC RUMB. */ + { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 00h ROM ONLY + { GB_MBC1 , GB_STANDARD_MBC, false, false, false, false}, // 01h MBC1 + { GB_MBC1 , GB_STANDARD_MBC, true , false, false, false}, // 02h MBC1+RAM + { GB_MBC1 , GB_STANDARD_MBC, true , true , false, false}, // 03h MBC1+RAM+BATTERY + [5] = + { GB_MBC2 , GB_STANDARD_MBC, true , false, false, false}, // 05h MBC2 + { GB_MBC2 , GB_STANDARD_MBC, true , true , false, false}, // 06h MBC2+BATTERY + [8] = + { GB_NO_MBC, GB_STANDARD_MBC, true , false, false, false}, // 08h ROM+RAM + { GB_NO_MBC, GB_STANDARD_MBC, true , true , false, false}, // 09h ROM+RAM+BATTERY + [0xB] = + /* Todo: Not supported yet */ + { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Bh MMM01 + { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Ch MMM01+RAM + { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Dh MMM01+RAM+BATTERY + [0xF] = + { GB_MBC3 , GB_STANDARD_MBC, false, true, true , false}, // 0Fh MBC3+TIMER+BATTERY + { GB_MBC3 , GB_STANDARD_MBC, true , true, true , false}, // 10h MBC3+TIMER+RAM+BATTERY + { GB_MBC3 , GB_STANDARD_MBC, false, false, false, false}, // 11h MBC3 + { GB_MBC3 , GB_STANDARD_MBC, true , false, false, false}, // 12h MBC3+RAM + { GB_MBC3 , GB_STANDARD_MBC, true , true , false, false}, // 13h MBC3+RAM+BATTERY + [0x19] = + { GB_MBC5 , GB_STANDARD_MBC, false, false, false, false}, // 19h MBC5 + { GB_MBC5 , GB_STANDARD_MBC, true , false, false, false}, // 1Ah MBC5+RAM + { GB_MBC5 , GB_STANDARD_MBC, true , true , false, false}, // 1Bh MBC5+RAM+BATTERY + { GB_MBC5 , GB_STANDARD_MBC, false, false, false, true }, // 1Ch MBC5+RUMBLE + { GB_MBC5 , GB_STANDARD_MBC, true , false, false, true }, // 1Dh MBC5+RUMBLE+RAM + { GB_MBC5 , GB_STANDARD_MBC, true , true , false, true }, // 1Eh MBC5+RUMBLE+RAM+BATTERY + [0xFC] = + { GB_MBC5 , GB_CAMERA , true , true , false, false}, // FCh POCKET CAMERA + { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // FDh BANDAI TAMA5 (Todo: Not supported) + { GB_HUC3 , GB_STANDARD_MBC, true , true , true, false}, // FEh HuC3 + { GB_HUC1 , GB_STANDARD_MBC, true , true , false, false}, // FFh HuC1+RAM+BATTERY +}; + +void GB_update_mbc_mappings(GB_gameboy_t *gb) +{ + switch (gb->cartridge_type->mbc_type) { + case GB_NO_MBC: return; + case GB_MBC1: + switch (gb->mbc1_wiring) { + case GB_STANDARD_MBC1_WIRING: + gb->mbc_rom_bank = gb->mbc1.bank_low | (gb->mbc1.bank_high << 5); + if (gb->mbc1.mode == 0) { + gb->mbc_ram_bank = 0; + gb->mbc_rom0_bank = 0; + } + else { + gb->mbc_ram_bank = gb->mbc1.bank_high; + gb->mbc_rom0_bank = gb->mbc1.bank_high << 5; + } + if ((gb->mbc_rom_bank & 0x1F) == 0) { + gb->mbc_rom_bank++; + } + break; + case GB_MBC1M_WIRING: + gb->mbc_rom_bank = (gb->mbc1.bank_low & 0xF) | (gb->mbc1.bank_high << 4); + if (gb->mbc1.mode == 0) { + gb->mbc_ram_bank = 0; + gb->mbc_rom0_bank = 0; + } + else { + gb->mbc_rom0_bank = gb->mbc1.bank_high << 4; + gb->mbc_ram_bank = 0; + } + if ((gb->mbc1.bank_low & 0x1F) == 0) { + gb->mbc_rom_bank++; + } + break; + } + break; + case GB_MBC2: + gb->mbc_rom_bank = gb->mbc2.rom_bank; + if ((gb->mbc_rom_bank & 0xF) == 0) { + gb->mbc_rom_bank = 1; + } + break; + case GB_MBC3: + gb->mbc_rom_bank = gb->mbc3.rom_bank; + gb->mbc_ram_bank = gb->mbc3.ram_bank; + if (!gb->is_mbc30) { + gb->mbc_rom_bank &= 0x7F; + } + if (gb->mbc_rom_bank == 0) { + gb->mbc_rom_bank = 1; + } + break; + case GB_MBC5: + gb->mbc_rom_bank = gb->mbc5.rom_bank_low | (gb->mbc5.rom_bank_high << 8); + gb->mbc_ram_bank = gb->mbc5.ram_bank; + break; + case GB_HUC1: + if (gb->huc1.mode == 0) { + gb->mbc_rom_bank = gb->huc1.bank_low | (gb->mbc1.bank_high << 6); + gb->mbc_ram_bank = 0; + } + else { + gb->mbc_rom_bank = gb->huc1.bank_low; + gb->mbc_ram_bank = gb->huc1.bank_high; + } + break; + case GB_HUC3: + gb->mbc_rom_bank = gb->huc3.rom_bank; + gb->mbc_ram_bank = gb->huc3.ram_bank; + break; + } +} + +void GB_configure_cart(GB_gameboy_t *gb) +{ + gb->cartridge_type = &GB_cart_defs[gb->rom[0x147]]; + + if (gb->rom[0x147] == 0 && gb->rom_size > 0x8000) { + GB_log(gb, "ROM header reports no MBC, but file size is over 32Kb. Assuming cartridge uses MBC3.\n"); + gb->cartridge_type = &GB_cart_defs[0x11]; + } + else if (gb->rom[0x147] != 0 && memcmp(gb->cartridge_type, &GB_cart_defs[0], sizeof(GB_cart_defs[0])) == 0) { + GB_log(gb, "Cartridge type %02x is not yet supported.\n", gb->rom[0x147]); + } + + if (gb->cartridge_type->has_ram) { + if (gb->cartridge_type->mbc_type == GB_MBC2) { + gb->mbc_ram_size = 0x200; + } + else { + static const unsigned ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000}; + gb->mbc_ram_size = ram_sizes[gb->rom[0x149]]; + } + + if (gb->mbc_ram_size) { + gb->mbc_ram = malloc(gb->mbc_ram_size); + } + + /* Todo: Some games assume unintialized MBC RAM is 0xFF. It this true for all cartridges types? */ + memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); + } + + /* MBC1 has at least 3 types of wiring (We currently support two (Standard and 4bit-MBC1M) of these). + See http://forums.nesdev.com/viewtopic.php?f=20&t=14099 */ + + /* Attempt to "guess" wiring */ + if (gb->cartridge_type->mbc_type == GB_MBC1) { + if (gb->rom_size >= 0x44000 && memcmp(gb->rom + 0x104, gb->rom + 0x40104, 0x30) == 0) { + gb->mbc1_wiring = GB_MBC1M_WIRING; + } + } + + /* Detect MBC30 */ + if (gb->cartridge_type->mbc_type == GB_MBC3) { + if (gb->rom_size > 0x200000 || gb->mbc_ram_size > 0x8000) { + gb->is_mbc30 = true; + } + } + + /* Set MBC5's bank to 1 correctly */ + if (gb->cartridge_type->mbc_type == GB_MBC5) { + gb->mbc5.rom_bank_low = 1; + } +} diff --git a/bsnes/gb/Core/mbc.h b/bsnes/gb/Core/mbc.h new file mode 100644 index 00000000..6a23300f --- /dev/null +++ b/bsnes/gb/Core/mbc.h @@ -0,0 +1,32 @@ +#ifndef MBC_h +#define MBC_h +#include "gb_struct_def.h" +#include + +typedef struct { + enum { + GB_NO_MBC, + GB_MBC1, + GB_MBC2, + GB_MBC3, + GB_MBC5, + GB_HUC1, + GB_HUC3, + } mbc_type; + enum { + GB_STANDARD_MBC, + GB_CAMERA, + } mbc_subtype; + bool has_ram; + bool has_battery; + bool has_rtc; + bool has_rumble; +} GB_cartridge_t; + +#ifdef GB_INTERNAL +extern const GB_cartridge_t GB_cart_defs[256]; +void GB_update_mbc_mappings(GB_gameboy_t *gb); +void GB_configure_cart(GB_gameboy_t *gb); +#endif + +#endif /* MBC_h */ diff --git a/bsnes/gb/Core/memory.c b/bsnes/gb/Core/memory.c new file mode 100644 index 00000000..f73209e3 --- /dev/null +++ b/bsnes/gb/Core/memory.c @@ -0,0 +1,1213 @@ +#include +#include +#include "gb.h" + +typedef uint8_t GB_read_function_t(GB_gameboy_t *gb, uint16_t addr); +typedef void GB_write_function_t(GB_gameboy_t *gb, uint16_t addr, uint8_t value); + +typedef enum { + GB_BUS_MAIN, /* In DMG: Cart and RAM. In CGB: Cart only */ + GB_BUS_RAM, /* In CGB only. */ + GB_BUS_VRAM, + GB_BUS_INTERNAL, /* Anything in highram. Might not be the most correct name. */ +} GB_bus_t; + +static GB_bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr) +{ + if (addr < 0x8000) { + return GB_BUS_MAIN; + } + if (addr < 0xA000) { + return GB_BUS_VRAM; + } + if (addr < 0xC000) { + return GB_BUS_MAIN; + } + if (addr < 0xFE00) { + return GB_is_cgb(gb)? GB_BUS_RAM : GB_BUS_MAIN; + } + return GB_BUS_INTERNAL; +} + +static uint8_t bitwise_glitch(uint8_t a, uint8_t b, uint8_t c) +{ + return ((a ^ c) & (b ^ c)) ^ c; +} + +static uint8_t bitwise_glitch_read(uint8_t a, uint8_t b, uint8_t c) +{ + return b | (a & c); +} + +static uint8_t bitwise_glitch_read_increase(uint8_t a, uint8_t b, uint8_t c, uint8_t d) +{ + return (b & (a | c | d)) | (a & c & d); +} + +void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address) +{ + if (GB_is_cgb(gb)) return; + + if (address >= 0xFE00 && address < 0xFF00) { + if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) { + gb->oam[gb->accessed_oam_row] = bitwise_glitch(gb->oam[gb->accessed_oam_row], + gb->oam[gb->accessed_oam_row - 8], + gb->oam[gb->accessed_oam_row - 4]); + gb->oam[gb->accessed_oam_row + 1] = bitwise_glitch(gb->oam[gb->accessed_oam_row + 1], + gb->oam[gb->accessed_oam_row - 7], + gb->oam[gb->accessed_oam_row - 3]); + for (unsigned i = 2; i < 8; i++) { + gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 8 + i]; + } + } + } +} + +void GB_trigger_oam_bug_read(GB_gameboy_t *gb, uint16_t address) +{ + if (GB_is_cgb(gb)) return; + + if (address >= 0xFE00 && address < 0xFF00) { + if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) { + gb->oam[gb->accessed_oam_row - 8] = + gb->oam[gb->accessed_oam_row] = bitwise_glitch_read(gb->oam[gb->accessed_oam_row], + gb->oam[gb->accessed_oam_row - 8], + gb->oam[gb->accessed_oam_row - 4]); + gb->oam[gb->accessed_oam_row - 7] = + gb->oam[gb->accessed_oam_row + 1] = bitwise_glitch_read(gb->oam[gb->accessed_oam_row + 1], + gb->oam[gb->accessed_oam_row - 7], + gb->oam[gb->accessed_oam_row - 3]); + for (unsigned i = 2; i < 8; i++) { + gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 8 + i]; + } + } + } +} + +void GB_trigger_oam_bug_read_increase(GB_gameboy_t *gb, uint16_t address) +{ + if (GB_is_cgb(gb)) return; + + if (address >= 0xFE00 && address < 0xFF00) { + if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 0x20 && gb->accessed_oam_row < 0x98) { + gb->oam[gb->accessed_oam_row - 0x8] = bitwise_glitch_read_increase(gb->oam[gb->accessed_oam_row - 0x10], + gb->oam[gb->accessed_oam_row - 0x08], + gb->oam[gb->accessed_oam_row ], + gb->oam[gb->accessed_oam_row - 0x04] + ); + gb->oam[gb->accessed_oam_row - 0x7] = bitwise_glitch_read_increase(gb->oam[gb->accessed_oam_row - 0x0f], + gb->oam[gb->accessed_oam_row - 0x07], + gb->oam[gb->accessed_oam_row + 0x01], + gb->oam[gb->accessed_oam_row - 0x03] + ); + for (unsigned i = 0; i < 8; i++) { + gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 0x10 + i] = gb->oam[gb->accessed_oam_row - 0x08 + i]; + } + } + } +} + +static bool is_addr_in_dma_use(GB_gameboy_t *gb, uint16_t addr) +{ + if (!gb->dma_steps_left || (gb->dma_cycles < 0 && !gb->is_dma_restarting) || addr >= 0xFE00) return false; + return bus_for_addr(gb, addr) == bus_for_addr(gb, gb->dma_current_src); +} + +static bool effective_ir_input(GB_gameboy_t *gb) +{ + return gb->infrared_input || (gb->io_registers[GB_IO_RP] & 1) || gb->cart_ir; +} + +static uint8_t read_rom(GB_gameboy_t *gb, uint16_t addr) +{ + if (addr < 0x100 && !gb->boot_rom_finished) { + return gb->boot_rom[addr]; + } + + if (addr >= 0x200 && addr < 0x900 && GB_is_cgb(gb) && !gb->boot_rom_finished) { + return gb->boot_rom[addr]; + } + + if (!gb->rom_size) { + return 0xFF; + } + unsigned effective_address = (addr & 0x3FFF) + gb->mbc_rom0_bank * 0x4000; + return gb->rom[effective_address & (gb->rom_size - 1)]; +} + +static uint8_t read_mbc_rom(GB_gameboy_t *gb, uint16_t addr) +{ + unsigned effective_address = (addr & 0x3FFF) + gb->mbc_rom_bank * 0x4000; + return gb->rom[effective_address & (gb->rom_size - 1)]; +} + +static uint8_t read_vram(GB_gameboy_t *gb, uint16_t addr) +{ + if (gb->vram_read_blocked) { + return 0xFF; + } + if (gb->display_state == 22 && GB_is_cgb(gb) && !gb->cgb_double_speed) { + if (addr & 0x1000) { + addr = gb->last_tile_index_address; + } + else if (gb->last_tile_data_address & 0x1000) { + /* TODO: This is case is more complicated then the rest and differ between revisions + It's probably affected by how VRAM is layed out, might be easier after a decap is done*/ + } + else { + addr = gb->last_tile_data_address; + } + } + return gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000]; +} + +static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) +{ + if (gb->cartridge_type->mbc_type == GB_HUC3) { + switch (gb->huc3_mode) { + case 0xC: // RTC read + if (gb->huc3_access_flags == 0x2) { + return 1; + } + return gb->huc3_read; + case 0xD: // RTC status + return 1; + case 0xE: // IR mode + return effective_ir_input(gb); // TODO: What are the other bits? + default: + GB_log(gb, "Unsupported HuC-3 mode %x read: %04x\n", gb->huc3_mode, addr); + return 1; // TODO: What happens in this case? + case 0: // TODO: R/O RAM? (or is it disabled?) + case 0xA: // RAM + break; + } + } + + if ((!gb->mbc_ram_enable) && + gb->cartridge_type->mbc_subtype != GB_CAMERA && + gb->cartridge_type->mbc_type != GB_HUC1 && + gb->cartridge_type->mbc_type != GB_HUC3) { + return 0xFF; + } + + if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) { + return 0xc0 | effective_ir_input(gb); + } + + if (gb->cartridge_type->has_rtc && gb->cartridge_type->mbc_type != GB_HUC3 && + gb->mbc3_rtc_mapped && gb->mbc_ram_bank <= 4) { + /* RTC read */ + gb->rtc_latched.high |= ~0xC1; /* Not all bytes in RTC high are used. */ + return gb->rtc_latched.data[gb->mbc_ram_bank]; + } + + if (gb->camera_registers_mapped) { + return GB_camera_read_register(gb, addr); + } + + if (!gb->mbc_ram || !gb->mbc_ram_size) { + return 0xFF; + } + + if (gb->cartridge_type->mbc_subtype == GB_CAMERA && gb->mbc_ram_bank == 0 && addr >= 0xa100 && addr < 0xaf00) { + return GB_camera_read_image(gb, addr - 0xa100); + } + + uint8_t effective_bank = gb->mbc_ram_bank; + if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) { + effective_bank &= 0x3; + } + uint8_t ret = gb->mbc_ram[((addr & 0x1FFF) + effective_bank * 0x2000) & (gb->mbc_ram_size - 1)]; + if (gb->cartridge_type->mbc_type == GB_MBC2) { + ret |= 0xF0; + } + return ret; +} + +static uint8_t read_ram(GB_gameboy_t *gb, uint16_t addr) +{ + return gb->ram[addr & 0x0FFF]; +} + +static uint8_t read_banked_ram(GB_gameboy_t *gb, uint16_t addr) +{ + return gb->ram[(addr & 0x0FFF) + gb->cgb_ram_bank * 0x1000]; +} + +static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) +{ + + if (gb->hdma_on) { + return gb->last_opcode_read; + } + + if (addr < 0xFE00) { + return read_banked_ram(gb, addr); + } + + if (addr < 0xFF00) { + if (gb->oam_write_blocked && !GB_is_cgb(gb)) { + GB_trigger_oam_bug_read(gb, addr); + return 0xff; + } + + if ((gb->dma_steps_left && (gb->dma_cycles > 0 || gb->is_dma_restarting))) { + /* Todo: Does reading from OAM during DMA causes the OAM bug? */ + return 0xff; + } + + if (gb->oam_read_blocked) { + if (!GB_is_cgb(gb)) { + if (addr < 0xFEA0) { + if (gb->accessed_oam_row == 0) { + gb->oam[(addr & 0xf8)] = + gb->oam[0] = bitwise_glitch_read(gb->oam[0], + gb->oam[(addr & 0xf8)], + gb->oam[(addr & 0xfe)]); + gb->oam[(addr & 0xf8) + 1] = + gb->oam[1] = bitwise_glitch_read(gb->oam[1], + gb->oam[(addr & 0xf8) + 1], + gb->oam[(addr & 0xfe) | 1]); + for (unsigned i = 2; i < 8; i++) { + gb->oam[i] = gb->oam[(addr & 0xf8) + i]; + } + } + else if (gb->accessed_oam_row == 0xa0) { + gb->oam[0x9e] = bitwise_glitch_read(gb->oam[0x9c], + gb->oam[0x9e], + gb->oam[(addr & 0xf8) | 6]); + gb->oam[0x9f] = bitwise_glitch_read(gb->oam[0x9d], + gb->oam[0x9f], + gb->oam[(addr & 0xf8) | 7]); + + for (unsigned i = 0; i < 8; i++) { + gb->oam[(addr & 0xf8) + i] = gb->oam[0x98 + i]; + } + } + } + } + return 0xff; + } + + if (addr < 0xFEA0) { + return gb->oam[addr & 0xFF]; + } + + if (gb->oam_read_blocked) { + return 0xFF; + } + + switch (gb->model) { + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + return (addr & 0xF0) | ((addr >> 4) & 0xF); + + /* + case GB_MODEL_CGB_D: + if (addr > 0xfec0) { + addr |= 0xf0; + } + return gb->extra_oam[addr - 0xfea0]; + */ + + case GB_MODEL_CGB_C: + /* + case GB_MODEL_CGB_B: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_0: + */ + addr &= ~0x18; + return gb->extra_oam[addr - 0xfea0]; + + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + break; + } + } + + if (addr < 0xFF00) { + + return 0; + + } + + if (addr < 0xFF80) { + switch (addr & 0xFF) { + case GB_IO_IF: + return gb->io_registers[GB_IO_IF] | 0xE0; + case GB_IO_TAC: + return gb->io_registers[GB_IO_TAC] | 0xF8; + case GB_IO_STAT: + return gb->io_registers[GB_IO_STAT] | 0x80; + case GB_IO_OPRI: + if (!GB_is_cgb(gb)) { + return 0xFF; + } + return gb->io_registers[GB_IO_OPRI] | 0xFE; + + case GB_IO_PCM_12: + if (!GB_is_cgb(gb)) return 0xFF; + return ((gb->apu.is_active[GB_SQUARE_2] ? (gb->apu.samples[GB_SQUARE_2] << 4) : 0) | + (gb->apu.is_active[GB_SQUARE_1] ? (gb->apu.samples[GB_SQUARE_1]) : 0)) & (gb->model <= GB_MODEL_CGB_C? gb->apu.pcm_mask[0] : 0xFF); + case GB_IO_PCM_34: + if (!GB_is_cgb(gb)) return 0xFF; + return ((gb->apu.is_active[GB_NOISE] ? (gb->apu.samples[GB_NOISE] << 4) : 0) | + (gb->apu.is_active[GB_WAVE] ? (gb->apu.samples[GB_WAVE]) : 0)) & (gb->model <= GB_MODEL_CGB_C? gb->apu.pcm_mask[1] : 0xFF); + case GB_IO_JOYP: + GB_timing_sync(gb); + case GB_IO_TMA: + case GB_IO_LCDC: + case GB_IO_SCY: + case GB_IO_SCX: + case GB_IO_LY: + case GB_IO_LYC: + case GB_IO_BGP: + case GB_IO_OBP0: + case GB_IO_OBP1: + case GB_IO_WY: + case GB_IO_WX: + case GB_IO_SC: + case GB_IO_SB: + case GB_IO_DMA: + return gb->io_registers[addr & 0xFF]; + case GB_IO_TIMA: + if (gb->tima_reload_state == GB_TIMA_RELOADING) { + return 0; + } + return gb->io_registers[GB_IO_TIMA]; + case GB_IO_DIV: + return gb->div_counter >> 8; + case GB_IO_HDMA5: + if (!gb->cgb_mode) return 0xFF; + return ((gb->hdma_on || gb->hdma_on_hblank)? 0 : 0x80) | ((gb->hdma_steps_left - 1) & 0x7F); + case GB_IO_SVBK: + if (!gb->cgb_mode) { + return 0xFF; + } + return gb->cgb_ram_bank | ~0x7; + case GB_IO_VBK: + if (!GB_is_cgb(gb)) { + return 0xFF; + } + return gb->cgb_vram_bank | ~0x1; + + /* Todo: It seems that a CGB in DMG mode can access BGPI and OBPI, but not BGPD and OBPD? */ + case GB_IO_BGPI: + case GB_IO_OBPI: + if (!GB_is_cgb(gb)) { + return 0xFF; + } + return gb->io_registers[addr & 0xFF] | 0x40; + + case GB_IO_BGPD: + case GB_IO_OBPD: + { + if (!gb->cgb_mode && gb->boot_rom_finished) { + return 0xFF; + } + if (gb->cgb_palettes_blocked) { + return 0xFF; + } + uint8_t index_reg = (addr & 0xFF) - 1; + return ((addr & 0xFF) == GB_IO_BGPD? + gb->background_palettes_data : + gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F]; + } + + case GB_IO_KEY1: + if (!gb->cgb_mode) { + return 0xFF; + } + return (gb->io_registers[GB_IO_KEY1] & 0x7F) | (gb->cgb_double_speed? 0xFE : 0x7E); + + case GB_IO_RP: { + if (!gb->cgb_mode) return 0xFF; + /* You will read your own IR LED if it's on. */ + uint8_t ret = (gb->io_registers[GB_IO_RP] & 0xC1) | 0x3C; + if ((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && effective_ir_input(gb)) { + ret |= 2; + } + return ret; + } + case GB_IO_UNKNOWN2: + case GB_IO_UNKNOWN3: + return GB_is_cgb(gb)? gb->io_registers[addr & 0xFF] : 0xFF; + case GB_IO_UNKNOWN4: + return gb->cgb_mode? gb->io_registers[addr & 0xFF] : 0xFF; + case GB_IO_UNKNOWN5: + return GB_is_cgb(gb)? gb->io_registers[addr & 0xFF] | 0x8F : 0xFF; + default: + if ((addr & 0xFF) >= GB_IO_NR10 && (addr & 0xFF) <= GB_IO_WAV_END) { + return GB_apu_read(gb, addr & 0xFF); + } + return 0xFF; + } + /* Hardware registers */ + return 0; + } + + if (addr == 0xFFFF) { + /* Interrupt Mask */ + return gb->interrupt_enable; + } + + /* HRAM */ + return gb->hram[addr - 0xFF80]; +} + +static GB_read_function_t * const read_map[] = +{ + read_rom, read_rom, read_rom, read_rom, /* 0XXX, 1XXX, 2XXX, 3XXX */ + read_mbc_rom, read_mbc_rom, read_mbc_rom, read_mbc_rom, /* 4XXX, 5XXX, 6XXX, 7XXX */ + read_vram, read_vram, /* 8XXX, 9XXX */ + read_mbc_ram, read_mbc_ram, /* AXXX, BXXX */ + read_ram, read_banked_ram, /* CXXX, DXXX */ + read_ram, read_high_memory, /* EXXX FXXX */ +}; + +void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t callback) +{ + gb->read_memory_callback = callback; +} + +uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr) +{ + if (gb->n_watchpoints) { + GB_debugger_test_read_watchpoint(gb, addr); + } + if (is_addr_in_dma_use(gb, addr)) { + addr = gb->dma_current_src; + } + uint8_t data = read_map[addr >> 12](gb, addr); + GB_apply_cheat(gb, addr, &data); + if (gb->read_memory_callback) { + data = gb->read_memory_callback(gb, addr, data); + } + return data; +} + +static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + switch (gb->cartridge_type->mbc_type) { + case GB_NO_MBC: return; + case GB_MBC1: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x2000: case 0x3000: gb->mbc1.bank_low = value; break; + case 0x4000: case 0x5000: gb->mbc1.bank_high = value; break; + case 0x6000: case 0x7000: gb->mbc1.mode = value; break; + } + break; + case GB_MBC2: + switch (addr & 0x4100) { + case 0x0000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x0100: gb->mbc2.rom_bank = value; break; + } + break; + case GB_MBC3: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x2000: case 0x3000: gb->mbc3.rom_bank = value; break; + case 0x4000: case 0x5000: + gb->mbc3.ram_bank = value; + gb->mbc3_rtc_mapped = value & 8; + break; + case 0x6000: case 0x7000: + if (!gb->rtc_latch && (value & 1)) { /* Todo: verify condition is correct */ + memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real)); + } + gb->rtc_latch = value & 1; + break; + } + break; + case GB_MBC5: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x2000: gb->mbc5.rom_bank_low = value; break; + case 0x3000: gb->mbc5.rom_bank_high = value; break; + case 0x4000: case 0x5000: + if (gb->cartridge_type->has_rumble) { + if (!!(value & 8) != gb->rumble_state) { + gb->rumble_state = !gb->rumble_state; + } + value &= 7; + } + gb->mbc5.ram_bank = value; + gb->camera_registers_mapped = (value & 0x10) && gb->cartridge_type->mbc_subtype == GB_CAMERA; + break; + } + break; + case GB_HUC1: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: gb->huc1.ir_mode = (value & 0xF) == 0xE; break; + case 0x2000: case 0x3000: gb->huc1.bank_low = value; break; + case 0x4000: case 0x5000: gb->huc1.bank_high = value; break; + case 0x6000: case 0x7000: gb->huc1.mode = value; break; + } + break; + case GB_HUC3: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: + gb->huc3_mode = value & 0xF; + gb->mbc_ram_enable = gb->huc3_mode == 0xA; + break; + case 0x2000: case 0x3000: gb->huc3.rom_bank = value; break; + case 0x4000: case 0x5000: gb->huc3.ram_bank = value; break; + } + break; + } + GB_update_mbc_mappings(gb); +} + +static void write_vram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + if (gb->vram_write_blocked) { + //GB_log(gb, "Wrote %02x to %04x (VRAM) during mode 3\n", value, addr); + return; + } + /* TODO: not verified */ + if (gb->display_state == 22 && GB_is_cgb(gb) && !gb->cgb_double_speed) { + if (addr & 0x1000) { + addr = gb->last_tile_index_address; + } + else if (gb->last_tile_data_address & 0x1000) { + /* TODO: This is case is more complicated then the rest and differ between revisions + It's probably affected by how VRAM is layed out, might be easier after a decap is done */ + } + else { + addr = gb->last_tile_data_address; + } + } + gb->vram[(addr & 0x1FFF) + (uint16_t) gb->cgb_vram_bank * 0x2000] = value; +} + +static bool huc3_write(GB_gameboy_t *gb, uint8_t value) +{ + switch (gb->huc3_mode) { + case 0xB: // RTC Write + switch (value >> 4) { + case 1: + if (gb->huc3_access_index < 3) { + gb->huc3_read = (gb->huc3_minutes >> (gb->huc3_access_index * 4)) & 0xF; + } + else if (gb->huc3_access_index < 7) { + gb->huc3_read = (gb->huc3_days >> ((gb->huc3_access_index - 3) * 4)) & 0xF; + } + else { + // GB_log(gb, "Attempting to read from unsupported HuC-3 register: %03x\n", gb->huc3_access_index); + } + gb->huc3_access_index++; + break; + case 2: + case 3: + if (gb->huc3_access_index < 3) { + gb->huc3_minutes &= ~(0xF << (gb->huc3_access_index * 4)); + gb->huc3_minutes |= ((value & 0xF) << (gb->huc3_access_index * 4)); + } + else if (gb->huc3_access_index < 7) { + gb->huc3_days &= ~(0xF << ((gb->huc3_access_index - 3) * 4)); + gb->huc3_days |= ((value & 0xF) << ((gb->huc3_access_index - 3) * 4)); + } + else if (gb->huc3_access_index >= 0x58 && gb->huc3_access_index <= 0x5a) { + gb->huc3_alarm_minutes &= ~(0xF << ((gb->huc3_access_index - 0x58) * 4)); + gb->huc3_alarm_minutes |= ((value & 0xF) << ((gb->huc3_access_index - 0x58) * 4)); + } + else if (gb->huc3_access_index >= 0x5b && gb->huc3_access_index <= 0x5e) { + gb->huc3_alarm_days &= ~(0xF << ((gb->huc3_access_index - 0x5b) * 4)); + gb->huc3_alarm_days |= ((value & 0xF) << ((gb->huc3_access_index - 0x5b) * 4)); + } + else if (gb->huc3_access_index == 0x5f) { + gb->huc3_alarm_enabled = value & 1; + } + else { + // GB_log(gb, "Attempting to write %x to unsupported HuC-3 register: %03x\n", value & 0xF, gb->huc3_access_index); + } + if ((value >> 4) == 3) { + gb->huc3_access_index++; + } + break; + case 4: + gb->huc3_access_index &= 0xF0; + gb->huc3_access_index |= value & 0xF; + break; + case 5: + gb->huc3_access_index &= 0x0F; + gb->huc3_access_index |= (value & 0xF) << 4; + break; + case 6: + gb->huc3_access_flags = (value & 0xF); + break; + + default: + break; + } + + return true; + case 0xD: // RTC status + // Not sure what writes here mean, they're always 0xFE + return true; + case 0xE: { // IR mode + bool old_input = effective_ir_input(gb); + gb->cart_ir = value & 1; + bool new_input = effective_ir_input(gb); + if (new_input != old_input) { + if (gb->infrared_callback) { + gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); + } + gb->cycles_since_ir_change = 0; + } + return true; + } + case 0xC: + return true; + default: + return false; + case 0: // Disabled + case 0xA: // RAM + return false; + } +} + +static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + if (gb->cartridge_type->mbc_type == GB_HUC3) { + if (huc3_write(gb, value)) return; + } + + if (gb->camera_registers_mapped) { + GB_camera_write_register(gb, addr, value); + return; + } + + if ((!gb->mbc_ram_enable) + && gb->cartridge_type->mbc_type != GB_HUC1) return; + + if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) { + bool old_input = effective_ir_input(gb); + gb->cart_ir = value & 1; + bool new_input = effective_ir_input(gb); + if (new_input != old_input) { + if (gb->infrared_callback) { + gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); + } + gb->cycles_since_ir_change = 0; + } + return; + } + + if (gb->cartridge_type->has_rtc && gb->mbc3_rtc_mapped && gb->mbc_ram_bank <= 4) { + gb->rtc_latched.data[gb->mbc_ram_bank] = gb->rtc_real.data[gb->mbc_ram_bank] = value; + return; + } + + if (!gb->mbc_ram || !gb->mbc_ram_size) { + return; + } + + uint8_t effective_bank = gb->mbc_ram_bank; + if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) { + effective_bank &= 0x3; + } + + gb->mbc_ram[((addr & 0x1FFF) + effective_bank * 0x2000) & (gb->mbc_ram_size - 1)] = value; +} + +static void write_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + gb->ram[addr & 0x0FFF] = value; +} + +static void write_banked_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + gb->ram[(addr & 0x0FFF) + gb->cgb_ram_bank * 0x1000] = value; +} + +static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + if (addr < 0xFE00) { + GB_log(gb, "Wrote %02x to %04x (RAM Mirror)\n", value, addr); + write_banked_ram(gb, addr, value); + return; + } + + if (addr < 0xFF00) { + if (gb->oam_write_blocked) { + GB_trigger_oam_bug(gb, addr); + return; + } + + if ((gb->dma_steps_left && (gb->dma_cycles > 0 || gb->is_dma_restarting))) { + /* Todo: Does writing to OAM during DMA causes the OAM bug? */ + return; + } + + if (GB_is_cgb(gb)) { + if (addr < 0xFEA0) { + gb->oam[addr & 0xFF] = value; + } + switch (gb->model) { + /* + case GB_MODEL_CGB_D: + if (addr > 0xfec0) { + addr |= 0xf0; + } + gb->extra_oam[addr - 0xfea0] = value; + break; + */ + case GB_MODEL_CGB_C: + /* + case GB_MODEL_CGB_B: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_0: + */ + addr &= ~0x18; + gb->extra_oam[addr - 0xfea0] = value; + break; + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + case GB_MODEL_CGB_E: + case GB_MODEL_AGB: + break; + } + return; + } + + if (addr < 0xFEA0) { + if (gb->accessed_oam_row == 0xa0) { + for (unsigned i = 0; i < 8; i++) { + if ((i & 6) != (addr & 6)) { + gb->oam[(addr & 0xf8) + i] = gb->oam[0x98 + i]; + } + else { + gb->oam[(addr & 0xf8) + i] = bitwise_glitch(gb->oam[(addr & 0xf8) + i], gb->oam[0x9c], gb->oam[0x98 + i]); + } + } + } + + gb->oam[addr & 0xFF] = value; + + if (gb->accessed_oam_row == 0) { + gb->oam[0] = bitwise_glitch(gb->oam[0], + gb->oam[(addr & 0xf8)], + gb->oam[(addr & 0xfe)]); + gb->oam[1] = bitwise_glitch(gb->oam[1], + gb->oam[(addr & 0xf8) + 1], + gb->oam[(addr & 0xfe) | 1]); + for (unsigned i = 2; i < 8; i++) { + gb->oam[i] = gb->oam[(addr & 0xf8) + i]; + } + } + } + else if (gb->accessed_oam_row == 0) { + gb->oam[addr & 0x7] = value; + } + return; + } + + /* Todo: Clean this code up: use a function table and move relevant code to display.c and timing.c + (APU read and writes are already at apu.c) */ + if (addr < 0xFF80) { + /* Hardware registers */ + switch (addr & 0xFF) { + case GB_IO_WY: + if (value == gb->current_line) { + gb->wy_triggered = true; + } + case GB_IO_WX: + case GB_IO_IF: + case GB_IO_SCX: + case GB_IO_SCY: + case GB_IO_BGP: + case GB_IO_OBP0: + case GB_IO_OBP1: + case GB_IO_SB: + case GB_IO_UNKNOWN2: + case GB_IO_UNKNOWN3: + case GB_IO_UNKNOWN4: + case GB_IO_UNKNOWN5: + gb->io_registers[addr & 0xFF] = value; + return; + case GB_IO_OPRI: + if ((!gb->boot_rom_finished || (gb->io_registers[GB_IO_KEY0] & 8)) && GB_is_cgb(gb)) { + gb->io_registers[addr & 0xFF] = value; + gb->object_priority = (value & 1) ? GB_OBJECT_PRIORITY_X : GB_OBJECT_PRIORITY_INDEX; + } + else if (gb->cgb_mode) { + gb->io_registers[addr & 0xFF] = value; + + } + return; + case GB_IO_LYC: + + /* TODO: Probably completely wrong in double speed mode */ + + /* TODO: This hack is disgusting */ + if (gb->display_state == 29 && GB_is_cgb(gb)) { + gb->ly_for_comparison = 153; + GB_STAT_update(gb); + gb->ly_for_comparison = 0; + } + + gb->io_registers[addr & 0xFF] = value; + + /* These are the states when LY changes, let the display routine call GB_STAT_update for use + so it correctly handles T-cycle accurate LYC writes */ + if (!GB_is_cgb(gb) || ( + gb->display_state != 35 && + gb->display_state != 26 && + gb->display_state != 15 && + gb->display_state != 16)) { + + /* More hacks to make LYC write conflicts work */ + if (gb->display_state == 14 && GB_is_cgb(gb)) { + gb->ly_for_comparison = 153; + GB_STAT_update(gb); + gb->ly_for_comparison = -1; + } + else { + GB_STAT_update(gb); + } + } + return; + + case GB_IO_TIMA: + if (gb->tima_reload_state != GB_TIMA_RELOADED) { + gb->io_registers[GB_IO_TIMA] = value; + } + return; + + case GB_IO_TMA: + gb->io_registers[GB_IO_TMA] = value; + if (gb->tima_reload_state != GB_TIMA_RUNNING) { + gb->io_registers[GB_IO_TIMA] = value; + } + return; + + case GB_IO_TAC: + GB_emulate_timer_glitch(gb, gb->io_registers[GB_IO_TAC], value); + gb->io_registers[GB_IO_TAC] = value; + return; + + + case GB_IO_LCDC: + if ((value & 0x80) && !(gb->io_registers[GB_IO_LCDC] & 0x80)) { + gb->display_cycles = 0; + gb->display_state = 0; + if (GB_is_sgb(gb)) { + gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + } + else if (gb->frame_skip_state == GB_FRAMESKIP_SECOND_FRAME_RENDERED) { + gb->frame_skip_state = GB_FRAMESKIP_LCD_TURNED_ON; + } + } + else if (!(value & 0x80) && (gb->io_registers[GB_IO_LCDC] & 0x80)) { + /* Sync after turning off LCD */ + GB_timing_sync(gb); + GB_lcd_off(gb); + } + /* Handle disabling objects while already fetching an object */ + if ((gb->io_registers[GB_IO_LCDC] & 2) && !(value & 2)) { + if (gb->during_object_fetch) { + gb->cycles_for_line += gb->display_cycles; + gb->display_cycles = 0; + gb->object_fetch_aborted = true; + } + } + gb->io_registers[GB_IO_LCDC] = value; + if (!(value & 0x20)) { + gb->wx_triggered = false; + gb->wx166_glitch = false; + } + return; + + case GB_IO_STAT: + /* Delete previous R/W bits */ + gb->io_registers[GB_IO_STAT] &= 7; + /* Set them by value */ + gb->io_registers[GB_IO_STAT] |= value & ~7; + /* Set unused bit to 1 */ + gb->io_registers[GB_IO_STAT] |= 0x80; + + GB_STAT_update(gb); + return; + + case GB_IO_DIV: + /* Reset the div state machine */ + gb->div_state = 0; + gb->div_cycles = 0; + return; + + case GB_IO_JOYP: + if (gb->joyp_write_callback) { + gb->joyp_write_callback(gb, value); + GB_update_joyp(gb); + } + else if ((gb->io_registers[GB_IO_JOYP] & 0x30) != (value & 0x30)) { + GB_sgb_write(gb, value); + gb->io_registers[GB_IO_JOYP] = (value & 0xF0) | (gb->io_registers[GB_IO_JOYP] & 0x0F); + GB_update_joyp(gb); + } + return; + + case GB_IO_BANK: + gb->boot_rom_finished = true; + return; + + case GB_IO_KEY0: + if (GB_is_cgb(gb) && !gb->boot_rom_finished) { + gb->cgb_mode = !(value & 0xC); /* The real "contents" of this register aren't quite known yet. */ + gb->io_registers[GB_IO_KEY0] = value; + } + return; + + case GB_IO_DMA: + if (gb->dma_steps_left) { + /* This is not correct emulation, since we're not really delaying the second DMA. + One write that should have happened in the first DMA will not happen. However, + since that byte will be overwritten by the second DMA before it can actually be + read, it doesn't actually matter. */ + gb->is_dma_restarting = true; + } + gb->dma_cycles = -7; + gb->dma_current_dest = 0; + gb->dma_current_src = value << 8; + gb->dma_steps_left = 0xa0; + gb->io_registers[GB_IO_DMA] = value; + return; + case GB_IO_SVBK: + if (!gb->cgb_mode) { + return; + } + gb->cgb_ram_bank = value & 0x7; + if (!gb->cgb_ram_bank) { + gb->cgb_ram_bank++; + } + return; + case GB_IO_VBK: + if (!gb->cgb_mode) { + return; + } + gb->cgb_vram_bank = value & 0x1; + return; + + case GB_IO_BGPI: + case GB_IO_OBPI: + if (!GB_is_cgb(gb)) { + return; + } + gb->io_registers[addr & 0xFF] = value; + return; + case GB_IO_BGPD: + case GB_IO_OBPD: + if (!gb->cgb_mode && gb->boot_rom_finished) { + /* Todo: Due to the behavior of a broken Game & Watch Gallery 2 ROM on a real CGB. A proper test ROM + is required. */ + return; + } + + uint8_t index_reg = (addr & 0xFF) - 1; + if (gb->cgb_palettes_blocked) { + if (gb->io_registers[index_reg] & 0x80) { + gb->io_registers[index_reg]++; + gb->io_registers[index_reg] |= 0x80; + } + return; + } + ((addr & 0xFF) == GB_IO_BGPD? + gb->background_palettes_data : + gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F] = value; + GB_palette_changed(gb, (addr & 0xFF) == GB_IO_BGPD, gb->io_registers[index_reg] & 0x3F); + if (gb->io_registers[index_reg] & 0x80) { + gb->io_registers[index_reg]++; + gb->io_registers[index_reg] |= 0x80; + } + return; + case GB_IO_KEY1: + if (!gb->cgb_mode) { + return; + } + gb->io_registers[GB_IO_KEY1] = value; + return; + case GB_IO_HDMA1: + if (gb->cgb_mode) { + gb->hdma_current_src &= 0xF0; + gb->hdma_current_src |= value << 8; + } + return; + case GB_IO_HDMA2: + if (gb->cgb_mode) { + gb->hdma_current_src &= 0xFF00; + gb->hdma_current_src |= value & 0xF0; + } + return; + case GB_IO_HDMA3: + if (gb->cgb_mode) { + gb->hdma_current_dest &= 0xF0; + gb->hdma_current_dest |= value << 8; + } + return; + case GB_IO_HDMA4: + if (gb->cgb_mode) { + gb->hdma_current_dest &= 0x1F00; + gb->hdma_current_dest |= value & 0xF0; + } + return; + case GB_IO_HDMA5: + if (!gb->cgb_mode) return; + if ((value & 0x80) == 0 && gb->hdma_on_hblank) { + gb->hdma_on_hblank = false; + return; + } + gb->hdma_on = (value & 0x80) == 0; + gb->hdma_on_hblank = (value & 0x80) != 0; + if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0) { + gb->hdma_on = true; + } + gb->io_registers[GB_IO_HDMA5] = value; + gb->hdma_steps_left = (gb->io_registers[GB_IO_HDMA5] & 0x7F) + 1; + /* Todo: Verify this. Gambatte's DMA tests require this. */ + if (gb->hdma_current_dest + (gb->hdma_steps_left << 4) > 0xFFFF) { + gb->hdma_steps_left = (0x10000 - gb->hdma_current_dest) >> 4; + } + gb->hdma_cycles = -12; + return; + + /* Todo: what happens when starting a transfer during a transfer? + What happens when starting a transfer during external clock? + */ + case GB_IO_SC: + if (!gb->cgb_mode) { + value |= 2; + } + gb->io_registers[GB_IO_SC] = value | (~0x83); + if ((value & 0x80) && (value & 0x1) ) { + gb->serial_length = gb->cgb_mode && (value & 2)? 16 : 512; + gb->serial_count = 0; + /* Todo: This is probably incorrect for CGB's faster clock mode. */ + gb->serial_cycles &= 0xFF; + if (gb->serial_transfer_bit_start_callback) { + gb->serial_transfer_bit_start_callback(gb, gb->io_registers[GB_IO_SB] & 0x80); + } + } + else { + gb->serial_length = 0; + } + return; + + case GB_IO_RP: { + if (!GB_is_cgb(gb)) { + return; + } + bool old_input = effective_ir_input(gb); + gb->io_registers[GB_IO_RP] = value; + bool new_input = effective_ir_input(gb); + if (new_input != old_input) { + if (gb->infrared_callback) { + gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); + } + gb->cycles_since_ir_change = 0; + } + return; + } + + default: + if ((addr & 0xFF) >= GB_IO_NR10 && (addr & 0xFF) <= GB_IO_WAV_END) { + GB_apu_write(gb, addr & 0xFF, value); + return; + } + GB_log(gb, "Wrote %02x to %04x (HW Register)\n", value, addr); + return; + } + } + + if (addr == 0xFFFF) { + /* Interrupt mask */ + gb->interrupt_enable = value; + return; + } + + /* HRAM */ + gb->hram[addr - 0xFF80] = value; +} + + + +static GB_write_function_t * const write_map[] = +{ + write_mbc, write_mbc, write_mbc, write_mbc, /* 0XXX, 1XXX, 2XXX, 3XXX */ + write_mbc, write_mbc, write_mbc, write_mbc, /* 4XXX, 5XXX, 6XXX, 7XXX */ + write_vram, write_vram, /* 8XXX, 9XXX */ + write_mbc_ram, write_mbc_ram, /* AXXX, BXXX */ + write_ram, write_banked_ram, /* CXXX, DXXX */ + write_ram, write_high_memory, /* EXXX FXXX */ +}; + +void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + if (gb->n_watchpoints) { + GB_debugger_test_write_watchpoint(gb, addr, value); + } + if (is_addr_in_dma_use(gb, addr)) { + /* Todo: What should happen? Will this affect DMA? Will data be written? What and where? */ + return; + } + write_map[addr >> 12](gb, addr, value); +} + +void GB_dma_run(GB_gameboy_t *gb) +{ + while (gb->dma_cycles >= 4 && gb->dma_steps_left) { + /* Todo: measure this value */ + gb->dma_cycles -= 4; + gb->dma_steps_left--; + + if (gb->dma_current_src < 0xe000) { + gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src); + } + else { + /* Todo: Not correct on the CGB */ + gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src & ~0x2000); + } + + /* dma_current_src must be the correct value during GB_read_memory */ + gb->dma_current_src++; + if (!gb->dma_steps_left) { + gb->is_dma_restarting = false; + } + } +} + +void GB_hdma_run(GB_gameboy_t *gb) +{ + if (!gb->hdma_on) return; + + while (gb->hdma_cycles >= 0x4) { + gb->hdma_cycles -= 0x4; + + GB_write_memory(gb, 0x8000 | (gb->hdma_current_dest++ & 0x1FFF), GB_read_memory(gb, (gb->hdma_current_src++))); + + if ((gb->hdma_current_dest & 0xf) == 0) { + if (--gb->hdma_steps_left == 0) { + gb->hdma_on = false; + gb->hdma_on_hblank = false; + gb->hdma_starting = false; + gb->io_registers[GB_IO_HDMA5] &= 0x7F; + break; + } + if (gb->hdma_on_hblank) { + gb->hdma_on = false; + break; + } + } + } +} diff --git a/bsnes/gb/Core/memory.h b/bsnes/gb/Core/memory.h new file mode 100644 index 00000000..f0d03907 --- /dev/null +++ b/bsnes/gb/Core/memory.h @@ -0,0 +1,18 @@ +#ifndef memory_h +#define memory_h +#include "gb_struct_def.h" +#include + +typedef uint8_t (*GB_read_memory_callback_t)(GB_gameboy_t *gb, uint16_t addr, uint8_t data); +void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t callback); + +uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr); +void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +#ifdef GB_INTERNAL +void GB_dma_run(GB_gameboy_t *gb); +void GB_hdma_run(GB_gameboy_t *gb); +void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address); +void GB_trigger_oam_bug_read_increase(GB_gameboy_t *gb, uint16_t address); +#endif + +#endif /* memory_h */ diff --git a/bsnes/gb/Core/printer.c b/bsnes/gb/Core/printer.c new file mode 100644 index 00000000..f04e54dd --- /dev/null +++ b/bsnes/gb/Core/printer.c @@ -0,0 +1,216 @@ +#include "gb.h" + +/* TODO: Emulation is VERY basic and assumes the ROM correctly uses the printer's interface. + Incorrect usage is not correctly emulated, as it's not well documented, nor do I + have my own GB Printer to figure it out myself. + + It also does not currently emulate communication timeout, which means that a bug + might prevent the printer operation until the GameBoy is restarted. + + Also, field mask values are assumed. */ + +static void handle_command(GB_gameboy_t *gb) +{ + + switch (gb->printer.command_id) { + case GB_PRINTER_INIT_COMMAND: + gb->printer.status = 0; + gb->printer.image_offset = 0; + break; + + case GB_PRINTER_START_COMMAND: + if (gb->printer.command_length == 4) { + gb->printer.status = 6; /* Printing */ + uint32_t image[gb->printer.image_offset]; + uint8_t palette = gb->printer.command_data[2]; + uint32_t colors[4] = {gb->rgb_encode_callback(gb, 0xff, 0xff, 0xff), + gb->rgb_encode_callback(gb, 0xaa, 0xaa, 0xaa), + gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55), + gb->rgb_encode_callback(gb, 0x00, 0x00, 0x00)}; + for (unsigned i = 0; i < gb->printer.image_offset; i++) { + image[i] = colors[(palette >> (gb->printer.image[i] * 2)) & 3]; + } + + if (gb->printer_callback) { + gb->printer_callback(gb, image, gb->printer.image_offset / 160, + gb->printer.command_data[1] >> 4, gb->printer.command_data[1] & 7, + gb->printer.command_data[3] & 0x7F); + } + + gb->printer.image_offset = 0; + } + break; + + case GB_PRINTER_DATA_COMMAND: + if (gb->printer.command_length == GB_PRINTER_DATA_SIZE) { + gb->printer.image_offset %= sizeof(gb->printer.image); + gb->printer.status = 8; /* Received 0x280 bytes */ + + uint8_t *byte = gb->printer.command_data; + + for (unsigned row = 2; row--; ) { + for (unsigned tile_x = 0; tile_x < 160 / 8; tile_x++) { + for (unsigned y = 0; y < 8; y++, byte += 2) { + for (unsigned x_pixel = 0; x_pixel < 8; x_pixel++) { + gb->printer.image[gb->printer.image_offset + tile_x * 8 + x_pixel + y * 160] = + ((*byte) >> 7) | (((*(byte + 1)) >> 7) << 1); + (*byte) <<= 1; + (*(byte + 1)) <<= 1; + } + } + } + + gb->printer.image_offset += 8 * 160; + } + } + + case GB_PRINTER_NOP_COMMAND: + default: + break; + } +} + + +static void byte_reieve_completed(GB_gameboy_t *gb, uint8_t byte_received) +{ + gb->printer.byte_to_send = 0; + switch (gb->printer.command_state) { + case GB_PRINTER_COMMAND_MAGIC1: + if (byte_received != 0x88) { + return; + } + gb->printer.status &= ~1; + gb->printer.command_length = 0; + gb->printer.checksum = 0; + break; + + case GB_PRINTER_COMMAND_MAGIC2: + if (byte_received != 0x33) { + if (byte_received != 0x88) { + gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1; + } + return; + } + break; + + case GB_PRINTER_COMMAND_ID: + gb->printer.command_id = byte_received & 0xF; + break; + + case GB_PRINTER_COMMAND_COMPRESSION: + gb->printer.compression = byte_received & 1; + break; + + case GB_PRINTER_COMMAND_LENGTH_LOW: + gb->printer.length_left = byte_received; + break; + + case GB_PRINTER_COMMAND_LENGTH_HIGH: + gb->printer.length_left |= (byte_received & 3) << 8; + break; + + case GB_PRINTER_COMMAND_DATA: + if (gb->printer.command_length != GB_PRINTER_MAX_COMMAND_LENGTH) { + if (gb->printer.compression) { + if (!gb->printer.compression_run_lenth) { + gb->printer.compression_run_is_compressed = byte_received & 0x80; + gb->printer.compression_run_lenth = (byte_received & 0x7F) + 1 + gb->printer.compression_run_is_compressed; + } + else if (gb->printer.compression_run_is_compressed) { + while (gb->printer.compression_run_lenth) { + gb->printer.command_data[gb->printer.command_length++] = byte_received; + gb->printer.compression_run_lenth--; + if (gb->printer.command_length == GB_PRINTER_MAX_COMMAND_LENGTH) { + gb->printer.compression_run_lenth = 0; + } + } + } + else { + gb->printer.command_data[gb->printer.command_length++] = byte_received; + gb->printer.compression_run_lenth--; + } + } + else { + gb->printer.command_data[gb->printer.command_length++] = byte_received; + } + } + gb->printer.length_left--; + break; + + case GB_PRINTER_COMMAND_CHECKSUM_LOW: + gb->printer.checksum ^= byte_received; + break; + + case GB_PRINTER_COMMAND_CHECKSUM_HIGH: + gb->printer.checksum ^= byte_received << 8; + if (gb->printer.checksum) { + gb->printer.status |= 1; /* Checksum error*/ + gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1; + return; + } + gb->printer.byte_to_send = 0x81; + + break; + case GB_PRINTER_COMMAND_ACTIVE: + if ((gb->printer.command_id & 0xF) == GB_PRINTER_INIT_COMMAND) { + /* Games expect INIT commands to return 0? */ + gb->printer.byte_to_send = 0; + } + else { + gb->printer.byte_to_send = gb->printer.status; + } + break; + case GB_PRINTER_COMMAND_STATUS: + + /* Printing is done instantly, but let the game recieve a 6 (Printing) status at least once, for compatibility */ + if (gb->printer.status == 6) { + gb->printer.status = 4; /* Done */ + } + + gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1; + handle_command(gb); + return; + } + + if (gb->printer.command_state >= GB_PRINTER_COMMAND_ID && gb->printer.command_state < GB_PRINTER_COMMAND_CHECKSUM_LOW) { + gb->printer.checksum += byte_received; + } + + if (gb->printer.command_state != GB_PRINTER_COMMAND_DATA) { + gb->printer.command_state++; + } + + if (gb->printer.command_state == GB_PRINTER_COMMAND_DATA) { + if (gb->printer.length_left == 0) { + gb->printer.command_state++; + } + } +} + +static void serial_start(GB_gameboy_t *gb, bool bit_received) +{ + gb->printer.byte_being_received <<= 1; + gb->printer.byte_being_received |= bit_received; + gb->printer.bits_received++; + if (gb->printer.bits_received == 8) { + byte_reieve_completed(gb, gb->printer.byte_being_received); + gb->printer.bits_received = 0; + gb->printer.byte_being_received = 0; + } +} + +static bool serial_end(GB_gameboy_t *gb) +{ + bool ret = gb->printer.bit_to_send; + gb->printer.bit_to_send = gb->printer.byte_to_send & 0x80; + gb->printer.byte_to_send <<= 1; + return ret; +} + +void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback) +{ + memset(&gb->printer, 0, sizeof(gb->printer)); + GB_set_serial_transfer_bit_start_callback(gb, serial_start); + GB_set_serial_transfer_bit_end_callback(gb, serial_end); + gb->printer_callback = callback; +} diff --git a/bsnes/gb/Core/printer.h b/bsnes/gb/Core/printer.h new file mode 100644 index 00000000..b29650ff --- /dev/null +++ b/bsnes/gb/Core/printer.h @@ -0,0 +1,64 @@ +#ifndef printer_h +#define printer_h +#include +#include +#include "gb_struct_def.h" +#define GB_PRINTER_MAX_COMMAND_LENGTH 0x280 +#define GB_PRINTER_DATA_SIZE 0x280 + +typedef void (*GB_print_image_callback_t)(GB_gameboy_t *gb, + uint32_t *image, + uint8_t height, + uint8_t top_margin, + uint8_t bottom_margin, + uint8_t exposure); + + +typedef struct +{ + /* Communication state machine */ + + enum { + GB_PRINTER_COMMAND_MAGIC1, + GB_PRINTER_COMMAND_MAGIC2, + GB_PRINTER_COMMAND_ID, + GB_PRINTER_COMMAND_COMPRESSION, + GB_PRINTER_COMMAND_LENGTH_LOW, + GB_PRINTER_COMMAND_LENGTH_HIGH, + GB_PRINTER_COMMAND_DATA, + GB_PRINTER_COMMAND_CHECKSUM_LOW, + GB_PRINTER_COMMAND_CHECKSUM_HIGH, + GB_PRINTER_COMMAND_ACTIVE, + GB_PRINTER_COMMAND_STATUS, + } command_state : 8; + enum { + GB_PRINTER_INIT_COMMAND = 1, + GB_PRINTER_START_COMMAND = 2, + GB_PRINTER_DATA_COMMAND = 4, + GB_PRINTER_NOP_COMMAND = 0xF, + } command_id : 8; + bool compression; + uint16_t length_left; + uint8_t command_data[GB_PRINTER_MAX_COMMAND_LENGTH]; + uint16_t command_length; + uint16_t checksum; + uint8_t status; + uint8_t byte_to_send; + + uint8_t image[160 * 200]; + uint16_t image_offset; + + /* TODO: Delete me. */ + uint64_t padding; + + uint8_t compression_run_lenth; + bool compression_run_is_compressed; + + uint8_t bits_received; + uint8_t byte_being_received; + bool bit_to_send; +} GB_printer_t; + + +void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback); +#endif diff --git a/bsnes/gb/Core/random.c b/bsnes/gb/Core/random.c new file mode 100644 index 00000000..cc4d4d3a --- /dev/null +++ b/bsnes/gb/Core/random.c @@ -0,0 +1,38 @@ +#include "random.h" +#include + +static uint64_t seed; +static bool enabled = true; + +uint8_t GB_random(void) +{ + if (!enabled) return 0; + + seed *= 0x27BB2EE687B0B0FDL; + seed += 0xB504F32D; + return seed >> 56; +} + +uint32_t GB_random32(void) +{ + GB_random(); + return seed >> 32; +} + +void GB_random_seed(uint64_t new_seed) +{ + seed = new_seed; +} + +void GB_random_set_enabled(bool enable) +{ + enabled = enable; +} + +static void __attribute__((constructor)) init_seed(void) +{ + seed = time(NULL); + for (unsigned i = 64; i--;) { + GB_random(); + } +} diff --git a/bsnes/gb/Core/random.h b/bsnes/gb/Core/random.h new file mode 100644 index 00000000..8ab0e502 --- /dev/null +++ b/bsnes/gb/Core/random.h @@ -0,0 +1,12 @@ +#ifndef random_h +#define random_h + +#include +#include + +uint8_t GB_random(void); +uint32_t GB_random32(void); +void GB_random_seed(uint64_t seed); +void GB_random_set_enabled(bool enable); + +#endif /* random_h */ diff --git a/bsnes/gb/Core/rewind.c b/bsnes/gb/Core/rewind.c new file mode 100644 index 00000000..c3900d60 --- /dev/null +++ b/bsnes/gb/Core/rewind.c @@ -0,0 +1,208 @@ +#include "gb.h" +#include +#include +#include +#include + +static uint8_t *state_compress(const uint8_t *prev, const uint8_t *data, size_t uncompressed_size) +{ + size_t malloc_size = 0x1000; + uint8_t *compressed = malloc(malloc_size); + size_t counter_pos = 0; + size_t data_pos = sizeof(uint16_t); + bool prev_mode = true; + *(uint16_t *)compressed = 0; +#define COUNTER (*(uint16_t *)&compressed[counter_pos]) +#define DATA (compressed[data_pos]) + + while (uncompressed_size) { + if (prev_mode) { + if (*data == *prev && COUNTER != 0xffff) { + COUNTER++; + data++; + prev++; + uncompressed_size--; + } + else { + prev_mode = false; + counter_pos += sizeof(uint16_t); + data_pos = counter_pos + sizeof(uint16_t); + if (data_pos >= malloc_size) { + malloc_size *= 2; + compressed = realloc(compressed, malloc_size); + } + COUNTER = 0; + } + } + else { + if (*data != *prev && COUNTER != 0xffff) { + COUNTER++; + DATA = *data; + data_pos++; + data++; + prev++; + uncompressed_size--; + if (data_pos >= malloc_size) { + malloc_size *= 2; + compressed = realloc(compressed, malloc_size); + } + } + else { + prev_mode = true; + counter_pos = data_pos; + data_pos = counter_pos + sizeof(uint16_t); + if (counter_pos >= malloc_size - 1) { + malloc_size *= 2; + compressed = realloc(compressed, malloc_size); + } + COUNTER = 0; + } + } + } + + return realloc(compressed, data_pos); +#undef DATA +#undef COUNTER +} + + +static void state_decompress(const uint8_t *prev, uint8_t *data, uint8_t *dest, size_t uncompressed_size) +{ + size_t counter_pos = 0; + size_t data_pos = sizeof(uint16_t); + bool prev_mode = true; +#define COUNTER (*(uint16_t *)&data[counter_pos]) +#define DATA (data[data_pos]) + + while (uncompressed_size) { + if (prev_mode) { + if (COUNTER) { + COUNTER--; + *(dest++) = *(prev++); + uncompressed_size--; + } + else { + prev_mode = false; + counter_pos += sizeof(uint16_t); + data_pos = counter_pos + sizeof(uint16_t); + } + } + else { + if (COUNTER) { + COUNTER--; + *(dest++) = DATA; + data_pos++; + prev++; + uncompressed_size--; + } + else { + prev_mode = true; + counter_pos = data_pos; + data_pos += sizeof(uint16_t); + } + } + } +#undef DATA +#undef COUNTER +} + +void GB_rewind_push(GB_gameboy_t *gb) +{ + const size_t save_size = GB_get_save_state_size(gb); + if (!gb->rewind_sequences) { + if (gb->rewind_buffer_length) { + gb->rewind_sequences = malloc(sizeof(*gb->rewind_sequences) * gb->rewind_buffer_length); + memset(gb->rewind_sequences, 0, sizeof(*gb->rewind_sequences) * gb->rewind_buffer_length); + gb->rewind_pos = 0; + } + else { + return; + } + } + + if (gb->rewind_sequences[gb->rewind_pos].pos == GB_REWIND_FRAMES_PER_KEY) { + gb->rewind_pos++; + if (gb->rewind_pos == gb->rewind_buffer_length) { + gb->rewind_pos = 0; + } + if (gb->rewind_sequences[gb->rewind_pos].key_state) { + free(gb->rewind_sequences[gb->rewind_pos].key_state); + gb->rewind_sequences[gb->rewind_pos].key_state = NULL; + } + for (unsigned i = 0; i < GB_REWIND_FRAMES_PER_KEY; i++) { + if (gb->rewind_sequences[gb->rewind_pos].compressed_states[i]) { + free(gb->rewind_sequences[gb->rewind_pos].compressed_states[i]); + gb->rewind_sequences[gb->rewind_pos].compressed_states[i] = 0; + } + } + gb->rewind_sequences[gb->rewind_pos].pos = 0; + } + + if (!gb->rewind_sequences[gb->rewind_pos].key_state) { + gb->rewind_sequences[gb->rewind_pos].key_state = malloc(save_size); + GB_save_state_to_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state); + } + else { + uint8_t *save_state = malloc(save_size); + GB_save_state_to_buffer(gb, save_state); + gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos++] = + state_compress(gb->rewind_sequences[gb->rewind_pos].key_state, save_state, save_size); + free(save_state); + } + +} + +bool GB_rewind_pop(GB_gameboy_t *gb) +{ + if (!gb->rewind_sequences || !gb->rewind_sequences[gb->rewind_pos].key_state) { + return false; + } + + const size_t save_size = GB_get_save_state_size(gb); + if (gb->rewind_sequences[gb->rewind_pos].pos == 0) { + GB_load_state_from_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state, save_size); + free(gb->rewind_sequences[gb->rewind_pos].key_state); + gb->rewind_sequences[gb->rewind_pos].key_state = NULL; + gb->rewind_pos = gb->rewind_pos == 0? gb->rewind_buffer_length - 1 : gb->rewind_pos - 1; + return true; + } + + uint8_t *save_state = malloc(save_size); + state_decompress(gb->rewind_sequences[gb->rewind_pos].key_state, + gb->rewind_sequences[gb->rewind_pos].compressed_states[--gb->rewind_sequences[gb->rewind_pos].pos], + save_state, + save_size); + free(gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos]); + gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos] = NULL; + GB_load_state_from_buffer(gb, save_state, save_size); + free(save_state); + return true; +} + +void GB_rewind_free(GB_gameboy_t *gb) +{ + if (!gb->rewind_sequences) return; + for (unsigned i = 0; i < gb->rewind_buffer_length; i++) { + if (gb->rewind_sequences[i].key_state) { + free(gb->rewind_sequences[i].key_state); + } + for (unsigned j = 0; j < GB_REWIND_FRAMES_PER_KEY; j++) { + if (gb->rewind_sequences[i].compressed_states[j]) { + free(gb->rewind_sequences[i].compressed_states[j]); + } + } + } + free(gb->rewind_sequences); + gb->rewind_sequences = NULL; +} + +void GB_set_rewind_length(GB_gameboy_t *gb, double seconds) +{ + GB_rewind_free(gb); + if (seconds == 0) { + gb->rewind_buffer_length = 0; + } + else { + gb->rewind_buffer_length = (size_t) ceil(seconds * CPU_FREQUENCY / LCDC_PERIOD / GB_REWIND_FRAMES_PER_KEY); + } +} diff --git a/bsnes/gb/Core/rewind.h b/bsnes/gb/Core/rewind.h new file mode 100644 index 00000000..ad548410 --- /dev/null +++ b/bsnes/gb/Core/rewind.h @@ -0,0 +1,14 @@ +#ifndef rewind_h +#define rewind_h + +#include +#include "gb_struct_def.h" + +#ifdef GB_INTERNAL +void GB_rewind_push(GB_gameboy_t *gb); +void GB_rewind_free(GB_gameboy_t *gb); +#endif +bool GB_rewind_pop(GB_gameboy_t *gb); +void GB_set_rewind_length(GB_gameboy_t *gb, double seconds); + +#endif diff --git a/bsnes/gb/Core/rumble.c b/bsnes/gb/Core/rumble.c new file mode 100644 index 00000000..8cbe20d1 --- /dev/null +++ b/bsnes/gb/Core/rumble.c @@ -0,0 +1,53 @@ +#include "rumble.h" +#include "gb.h" + +void GB_set_rumble_mode(GB_gameboy_t *gb, GB_rumble_mode_t mode) +{ + gb->rumble_mode = mode; + if (gb->rumble_callback) { + gb->rumble_callback(gb, 0); + } +} + +void GB_handle_rumble(GB_gameboy_t *gb) +{ + if (gb->rumble_callback) { + if (gb->rumble_mode == GB_RUMBLE_DISABLED) { + return; + } + if (gb->cartridge_type->has_rumble) { + if (gb->rumble_on_cycles + gb->rumble_off_cycles) { + gb->rumble_callback(gb, gb->rumble_on_cycles / (double)(gb->rumble_on_cycles + gb->rumble_off_cycles)); + gb->rumble_on_cycles = gb->rumble_off_cycles = 0; + } + } + else if (gb->rumble_mode == GB_RUMBLE_ALL_GAMES) { + unsigned volume = (gb->io_registers[GB_IO_NR50] & 7) + 1 + ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1; + unsigned ch4_volume = volume * (!!(gb->io_registers[GB_IO_NR51] & 8) + !!(gb->io_registers[GB_IO_NR51] & 0x80)); + unsigned ch1_volume = volume * (!!(gb->io_registers[GB_IO_NR51] & 1) + !!(gb->io_registers[GB_IO_NR51] & 0x10)); + + double ch4_rumble = (MIN(gb->apu.noise_channel.sample_length * (gb->apu.noise_channel.narrow? 8 : 1) , 4096) * ((signed) gb->apu.noise_channel.current_volume * gb->apu.noise_channel.current_volume * ch4_volume / 32.0 - 50) - 2048) / 2048.0; + + ch4_rumble = MIN(ch4_rumble, 1.0); + ch4_rumble = MAX(ch4_rumble, 0.0); + + double ch1_rumble = 0; + if (gb->apu.sweep_enabled && ((gb->io_registers[GB_IO_NR10] >> 4) & 7)) { + double sweep_speed = (gb->io_registers[GB_IO_NR10] & 7) / (double)((gb->io_registers[GB_IO_NR10] >> 4) & 7); + ch1_rumble = gb->apu.square_channels[GB_SQUARE_1].current_volume * ch1_volume / 32.0 * sweep_speed / 8.0 - 0.5; + ch1_rumble = MIN(ch1_rumble, 1.0); + ch1_rumble = MAX(ch1_rumble, 0.0); + } + + if (!gb->apu.is_active[GB_NOISE]) { + ch4_rumble = 0; + } + + if (!gb->apu.is_active[GB_SQUARE_1]) { + ch1_rumble = 0; + } + + gb->rumble_callback(gb, MIN(MAX(ch1_rumble / 2 + ch4_rumble, 0.0), 1.0)); + } + } +} diff --git a/bsnes/gb/Core/rumble.h b/bsnes/gb/Core/rumble.h new file mode 100644 index 00000000..eae9f372 --- /dev/null +++ b/bsnes/gb/Core/rumble.h @@ -0,0 +1,17 @@ +#ifndef rumble_h +#define rumble_h + +#include "gb_struct_def.h" + +typedef enum { + GB_RUMBLE_DISABLED, + GB_RUMBLE_CARTRIDGE_ONLY, + GB_RUMBLE_ALL_GAMES +} GB_rumble_mode_t; + +#ifdef GB_INTERNAL +void GB_handle_rumble(GB_gameboy_t *gb); +#endif +void GB_set_rumble_mode(GB_gameboy_t *gb, GB_rumble_mode_t mode); + +#endif /* rumble_h */ diff --git a/bsnes/gb/Core/save_state.c b/bsnes/gb/Core/save_state.c new file mode 100644 index 00000000..9ef6ae35 --- /dev/null +++ b/bsnes/gb/Core/save_state.c @@ -0,0 +1,413 @@ +#include "gb.h" +#include +#include + +static bool dump_section(FILE *f, const void *src, uint32_t size) +{ + if (fwrite(&size, 1, sizeof(size), f) != sizeof(size)) { + return false; + } + + if (fwrite(src, 1, size, f) != size) { + return false; + } + + return true; +} + +#define DUMP_SECTION(gb, f, section) dump_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section)) + +/* Todo: we need a sane and protable save state format. */ +int GB_save_state(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "wb"); + if (!f) { + GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); + return errno; + } + + if (fwrite(GB_GET_SECTION(gb, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error; + if (!DUMP_SECTION(gb, f, core_state)) goto error; + if (!DUMP_SECTION(gb, f, dma )) goto error; + if (!DUMP_SECTION(gb, f, mbc )) goto error; + if (!DUMP_SECTION(gb, f, hram )) goto error; + if (!DUMP_SECTION(gb, f, timing )) goto error; + if (!DUMP_SECTION(gb, f, apu )) goto error; + if (!DUMP_SECTION(gb, f, rtc )) goto error; + if (!DUMP_SECTION(gb, f, video )) goto error; + + if (GB_is_hle_sgb(gb)) { + if (!dump_section(f, gb->sgb, sizeof(*gb->sgb))) goto error; + } + + if (fwrite(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) { + goto error; + } + + if (fwrite(gb->ram, 1, gb->ram_size, f) != gb->ram_size) { + goto error; + } + + if (fwrite(gb->vram, 1, gb->vram_size, f) != gb->vram_size) { + goto error; + } + + errno = 0; + +error: + fclose(f); + return errno; +} + +#undef DUMP_SECTION + +size_t GB_get_save_state_size(GB_gameboy_t *gb) +{ + return GB_SECTION_SIZE(header) + + GB_SECTION_SIZE(core_state) + sizeof(uint32_t) + + GB_SECTION_SIZE(dma ) + sizeof(uint32_t) + + GB_SECTION_SIZE(mbc ) + sizeof(uint32_t) + + GB_SECTION_SIZE(hram ) + sizeof(uint32_t) + + GB_SECTION_SIZE(timing ) + sizeof(uint32_t) + + GB_SECTION_SIZE(apu ) + sizeof(uint32_t) + + GB_SECTION_SIZE(rtc ) + sizeof(uint32_t) + + GB_SECTION_SIZE(video ) + sizeof(uint32_t) + + (GB_is_hle_sgb(gb)? sizeof(*gb->sgb) + sizeof(uint32_t) : 0) + + gb->mbc_ram_size + + gb->ram_size + + gb->vram_size; +} + +/* A write-line function for memory copying */ +static void buffer_write(const void *src, size_t size, uint8_t **dest) +{ + memcpy(*dest, src, size); + *dest += size; +} + +static void buffer_dump_section(uint8_t **buffer, const void *src, uint32_t size) +{ + buffer_write(&size, sizeof(size), buffer); + buffer_write(src, size, buffer); +} + +#define DUMP_SECTION(gb, buffer, section) buffer_dump_section(&buffer, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section)) +void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer) +{ + buffer_write(GB_GET_SECTION(gb, header), GB_SECTION_SIZE(header), &buffer); + DUMP_SECTION(gb, buffer, core_state); + DUMP_SECTION(gb, buffer, dma ); + DUMP_SECTION(gb, buffer, mbc ); + DUMP_SECTION(gb, buffer, hram ); + DUMP_SECTION(gb, buffer, timing ); + DUMP_SECTION(gb, buffer, apu ); + DUMP_SECTION(gb, buffer, rtc ); + DUMP_SECTION(gb, buffer, video ); + + if (GB_is_hle_sgb(gb)) { + buffer_dump_section(&buffer, gb->sgb, sizeof(*gb->sgb)); + } + + + buffer_write(gb->mbc_ram, gb->mbc_ram_size, &buffer); + buffer_write(gb->ram, gb->ram_size, &buffer); + buffer_write(gb->vram, gb->vram_size, &buffer); +} + +/* Best-effort read function for maximum future compatibility. */ +static bool read_section(FILE *f, void *dest, uint32_t size, bool fix_broken_windows_saves) +{ + uint32_t saved_size = 0; + if (fread(&saved_size, 1, sizeof(size), f) != sizeof(size)) { + return false; + } + + if (fix_broken_windows_saves) { + if (saved_size < 4) { + return false; + } + saved_size -= 4; + fseek(f, 4, SEEK_CUR); + } + + if (saved_size <= size) { + if (fread(dest, 1, saved_size, f) != saved_size) { + return false; + } + } + else { + if (fread(dest, 1, size, f) != size) { + return false; + } + fseek(f, saved_size - size, SEEK_CUR); + } + + return true; +} +#undef DUMP_SECTION + +static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save) +{ + if (save->ram_size == 0 && (&save->ram_size)[-1] == gb->ram_size) { + /* This is a save state with a bad printer struct from a 32-bit OS */ + memcpy(save->extra_oam + 4, save->extra_oam, (uintptr_t)&save->ram_size - (uintptr_t)&save->extra_oam); + } + if (save->ram_size == 0) { + /* Save doesn't have ram size specified, it's a pre 0.12 save state with potentially + incorrect RAM amount if it's a CGB instance */ + if (GB_is_cgb(save)) { + save->ram_size = 0x2000 * 8; // Incorrect RAM size + } + else { + save->ram_size = gb->ram_size; + } + } + + if (gb->version != save->version) { + GB_log(gb, "The save state is for a different version of SameBoy.\n"); + return false; + } + + if (gb->mbc_ram_size < save->mbc_ram_size) { + GB_log(gb, "The save state has non-matching MBC RAM size.\n"); + return false; + } + + if (gb->vram_size != save->vram_size) { + GB_log(gb, "The save state has non-matching VRAM size. Try changing the emulated model.\n"); + return false; + } + + if (GB_is_hle_sgb(gb) != GB_is_hle_sgb(save)) { + GB_log(gb, "The save state is %sfor a Super Game Boy. Try changing the emulated model.\n", GB_is_hle_sgb(save)? "" : "not "); + return false; + } + + if (gb->ram_size != save->ram_size) { + if (gb->ram_size == 0x1000 * 8 && save->ram_size == 0x2000 * 8) { + /* A bug in versions prior to 0.12 made CGB instances allocate twice the ammount of RAM. + Ignore this issue to retain compatibility with older, 0.11, save states. */ + } + else { + GB_log(gb, "The save state has non-matching RAM size. Try changing the emulated model.\n"); + return false; + } + } + + return true; +} + +static void sanitize_state(GB_gameboy_t *gb) +{ + for (unsigned i = 0; i < 32; i++) { + GB_palette_changed(gb, false, i * 2); + GB_palette_changed(gb, true, i * 2); + } + + gb->bg_fifo.read_end &= 0xF; + gb->bg_fifo.write_end &= 0xF; + gb->oam_fifo.read_end &= 0xF; + gb->oam_fifo.write_end &= 0xF; + gb->object_low_line_address &= gb->vram_size & ~1; + gb->fetcher_x &= 0x1f; + if (gb->lcd_x > gb->position_in_line) { + gb->lcd_x = gb->position_in_line; + } + + if (gb->object_priority == GB_OBJECT_PRIORITY_UNDEFINED) { + gb->object_priority = gb->cgb_mode? GB_OBJECT_PRIORITY_INDEX : GB_OBJECT_PRIORITY_X; + } +} + +#define READ_SECTION(gb, f, section) read_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section), fix_broken_windows_saves) + +int GB_load_state(GB_gameboy_t *gb, const char *path) +{ + GB_gameboy_t save; + + /* Every unread value should be kept the same. */ + memcpy(&save, gb, sizeof(save)); + /* ...Except ram size, we use it to detect old saves with incorrect ram sizes */ + save.ram_size = 0; + + FILE *f = fopen(path, "rb"); + if (!f) { + GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); + return errno; + } + + bool fix_broken_windows_saves = false; + if (fread(GB_GET_SECTION(&save, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error; + if (save.magic == 0) { + /* Potentially legacy, broken Windows save state */ + fseek(f, 4, SEEK_SET); + if (fread(GB_GET_SECTION(&save, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error; + fix_broken_windows_saves = true; + } + if (gb->magic != save.magic) { + GB_log(gb, "The file is not a save state, or is from an incompatible operating system.\n"); + return false; + } + if (!READ_SECTION(&save, f, core_state)) goto error; + if (!READ_SECTION(&save, f, dma )) goto error; + if (!READ_SECTION(&save, f, mbc )) goto error; + if (!READ_SECTION(&save, f, hram )) goto error; + if (!READ_SECTION(&save, f, timing )) goto error; + if (!READ_SECTION(&save, f, apu )) goto error; + if (!READ_SECTION(&save, f, rtc )) goto error; + if (!READ_SECTION(&save, f, video )) goto error; + + if (!verify_and_update_state_compatibility(gb, &save)) { + errno = -1; + goto error; + } + + if (GB_is_hle_sgb(gb)) { + if (!read_section(f, gb->sgb, sizeof(*gb->sgb), false)) goto error; + } + + memset(gb->mbc_ram + save.mbc_ram_size, 0xFF, gb->mbc_ram_size - save.mbc_ram_size); + if (fread(gb->mbc_ram, 1, save.mbc_ram_size, f) != save.mbc_ram_size) { + fclose(f); + return EIO; + } + + if (fread(gb->ram, 1, gb->ram_size, f) != gb->ram_size) { + fclose(f); + return EIO; + } + + /* Fix for 0.11 save states that allocate twice the amount of RAM in CGB instances */ + fseek(f, save.ram_size - gb->ram_size, SEEK_CUR); + + if (fread(gb->vram, 1, gb->vram_size, f) != gb->vram_size) { + fclose(f); + return EIO; + } + + size_t orig_ram_size = gb->ram_size; + memcpy(gb, &save, sizeof(save)); + gb->ram_size = orig_ram_size; + + errno = 0; + + sanitize_state(gb); + +error: + fclose(f); + return errno; +} + +#undef READ_SECTION + +/* An read-like function for buffer-copying */ +static size_t buffer_read(void *dest, size_t length, const uint8_t **buffer, size_t *buffer_length) +{ + if (length > *buffer_length) { + length = *buffer_length; + } + + memcpy(dest, *buffer, length); + *buffer += length; + *buffer_length -= length; + + return length; +} + +static bool buffer_read_section(const uint8_t **buffer, size_t *buffer_length, void *dest, uint32_t size, bool fix_broken_windows_saves) +{ + uint32_t saved_size = 0; + if (buffer_read(&saved_size, sizeof(size), buffer, buffer_length) != sizeof(size)) { + return false; + } + + if (saved_size > *buffer_length) return false; + + if (fix_broken_windows_saves) { + if (saved_size < 4) { + return false; + } + saved_size -= 4; + *buffer += 4; + } + + if (saved_size <= size) { + if (buffer_read(dest, saved_size, buffer, buffer_length) != saved_size) { + return false; + } + } + else { + if (buffer_read(dest, size, buffer, buffer_length) != size) { + return false; + } + *buffer += saved_size - size; + *buffer_length -= saved_size - size; + } + + return true; +} + +#define READ_SECTION(gb, buffer, length, section) buffer_read_section(&buffer, &length, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section), fix_broken_windows_saves) +int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length) +{ + GB_gameboy_t save; + + /* Every unread value should be kept the same. */ + memcpy(&save, gb, sizeof(save)); + bool fix_broken_windows_saves = false; + + if (buffer_read(GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header), &buffer, &length) != GB_SECTION_SIZE(header)) return -1; + if (save.magic == 0) { + /* Potentially legacy, broken Windows save state*/ + buffer -= GB_SECTION_SIZE(header) - 4; + length += GB_SECTION_SIZE(header) - 4; + if (buffer_read(GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header), &buffer, &length) != GB_SECTION_SIZE(header)) return -1; + fix_broken_windows_saves = true; + } + if (gb->magic != save.magic) { + GB_log(gb, "The file is not a save state, or is from an incompatible operating system.\n"); + return false; + } + if (!READ_SECTION(&save, buffer, length, core_state)) return -1; + if (!READ_SECTION(&save, buffer, length, dma )) return -1; + if (!READ_SECTION(&save, buffer, length, mbc )) return -1; + if (!READ_SECTION(&save, buffer, length, hram )) return -1; + if (!READ_SECTION(&save, buffer, length, timing )) return -1; + if (!READ_SECTION(&save, buffer, length, apu )) return -1; + if (!READ_SECTION(&save, buffer, length, rtc )) return -1; + if (!READ_SECTION(&save, buffer, length, video )) return -1; + + + if (!verify_and_update_state_compatibility(gb, &save)) { + return -1; + } + + if (GB_is_hle_sgb(gb)) { + if (!buffer_read_section(&buffer, &length, gb->sgb, sizeof(*gb->sgb), false)) return -1; + } + + memset(gb->mbc_ram + save.mbc_ram_size, 0xFF, gb->mbc_ram_size - save.mbc_ram_size); + if (buffer_read(gb->mbc_ram, save.mbc_ram_size, &buffer, &length) != save.mbc_ram_size) { + return -1; + } + + if (buffer_read(gb->ram, gb->ram_size, &buffer, &length) != gb->ram_size) { + return -1; + } + + if (buffer_read(gb->vram, gb->vram_size, &buffer, &length) != gb->vram_size) { + return -1; + } + + /* Fix for 0.11 save states that allocate twice the amount of RAM in CGB instances */ + buffer += save.ram_size - gb->ram_size; + length -= save.ram_size - gb->ram_size; + + memcpy(gb, &save, sizeof(save)); + + sanitize_state(gb); + + return 0; +} + +#undef READ_SECTION diff --git a/bsnes/gb/Core/save_state.h b/bsnes/gb/Core/save_state.h new file mode 100644 index 00000000..8e5fc4e0 --- /dev/null +++ b/bsnes/gb/Core/save_state.h @@ -0,0 +1,30 @@ +/* Macros to make the GB_gameboy_t struct more future compatible when state saving */ +#ifndef save_state_h +#define save_state_h +#include + +#define GB_PADDING(type, old_usage) type old_usage##__do_not_use + +#ifdef __cplusplus +/* For bsnes integration. C++ code does not need section information, and throws a fit over certain types such + as anonymous enums inside unions */ +#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) __VA_ARGS__ +#else +#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) union {uint8_t name##_section_start; struct {__VA_ARGS__};}; uint8_t name##_section_end[0] +#define GB_SECTION_OFFSET(name) (offsetof(GB_gameboy_t, name##_section_start)) +#define GB_SECTION_SIZE(name) (offsetof(GB_gameboy_t, name##_section_end) - offsetof(GB_gameboy_t, name##_section_start)) +#define GB_GET_SECTION(gb, name) ((void*)&((gb)->name##_section_start)) +#endif + +#define GB_aligned_double __attribute__ ((aligned (8))) double + + +/* Public calls related to save states */ +int GB_save_state(GB_gameboy_t *gb, const char *path); +size_t GB_get_save_state_size(GB_gameboy_t *gb); +/* Assumes buffer is big enough to contain the save state. Use with GB_get_save_state_size(). */ +void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer); + +int GB_load_state(GB_gameboy_t *gb, const char *path); +int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length); +#endif /* save_state_h */ diff --git a/bsnes/gb/Core/sgb.c b/bsnes/gb/Core/sgb.c new file mode 100644 index 00000000..c77b0db6 --- /dev/null +++ b/bsnes/gb/Core/sgb.c @@ -0,0 +1,913 @@ +#include "gb.h" +#include "random.h" +#include +#include + +#ifndef M_PI + #define M_PI 3.14159265358979323846 +#endif + +#define INTRO_ANIMATION_LENGTH 200 + +enum { + PAL01 = 0x00, + PAL23 = 0x01, + PAL03 = 0x02, + PAL12 = 0x03, + ATTR_BLK = 0x04, + ATTR_LIN = 0x05, + ATTR_DIV = 0x06, + ATTR_CHR = 0x07, + PAL_SET = 0x0A, + PAL_TRN = 0x0B, + DATA_SND = 0x0F, + MLT_REQ = 0x11, + CHR_TRN = 0x13, + PCT_TRN = 0x14, + ATTR_TRN = 0x15, + ATTR_SET = 0x16, + MASK_EN = 0x17, +}; + +typedef enum { + MASK_DISABLED, + MASK_FREEZE, + MASK_BLACK, + MASK_COLOR_0, +} mask_mode_t; + +typedef enum { + TRANSFER_LOW_TILES, + TRANSFER_HIGH_TILES, + TRANSFER_BORDER_DATA, + TRANSFER_PALETTES, + TRANSFER_ATTRIBUTES, +} transfer_dest_t; + +#define SGB_PACKET_SIZE 16 +static inline void pal_command(GB_gameboy_t *gb, unsigned first, unsigned second) +{ + gb->sgb->effective_palettes[0] = gb->sgb->effective_palettes[4] = + gb->sgb->effective_palettes[8] = gb->sgb->effective_palettes[12] = + gb->sgb->command[1] | (gb->sgb->command[2] << 8); + + for (unsigned i = 0; i < 3; i++) { + gb->sgb->effective_palettes[first * 4 + i + 1] = gb->sgb->command[3 + i * 2] | (gb->sgb->command[4 + i * 2] << 8); + } + + for (unsigned i = 0; i < 3; i++) { + gb->sgb->effective_palettes[second * 4 + i + 1] = gb->sgb->command[9 + i * 2] | (gb->sgb->command[10 + i * 2] << 8); + } +} + +static inline void load_attribute_file(GB_gameboy_t *gb, unsigned file_index) +{ + if (file_index > 0x2C) return; + uint8_t *output = gb->sgb->attribute_map; + for (unsigned i = 0; i < 90; i++) { + uint8_t byte = gb->sgb->attribute_files[file_index * 90 + i]; + for (unsigned j = 4; j--;) { + *(output++) = byte >> 6; + byte <<= 2; + } + } +} + +static const uint16_t built_in_palettes[] = +{ + 0x67BF, 0x265B, 0x10B5, 0x2866, + 0x637B, 0x3AD9, 0x0956, 0x0000, + 0x7F1F, 0x2A7D, 0x30F3, 0x4CE7, + 0x57FF, 0x2618, 0x001F, 0x006A, + 0x5B7F, 0x3F0F, 0x222D, 0x10EB, + 0x7FBB, 0x2A3C, 0x0015, 0x0900, + 0x2800, 0x7680, 0x01EF, 0x2FFF, + 0x73BF, 0x46FF, 0x0110, 0x0066, + 0x533E, 0x2638, 0x01E5, 0x0000, + 0x7FFF, 0x2BBF, 0x00DF, 0x2C0A, + 0x7F1F, 0x463D, 0x74CF, 0x4CA5, + 0x53FF, 0x03E0, 0x00DF, 0x2800, + 0x433F, 0x72D2, 0x3045, 0x0822, + 0x7FFA, 0x2A5F, 0x0014, 0x0003, + 0x1EED, 0x215C, 0x42FC, 0x0060, + 0x7FFF, 0x5EF7, 0x39CE, 0x0000, + 0x4F5F, 0x630E, 0x159F, 0x3126, + 0x637B, 0x121C, 0x0140, 0x0840, + 0x66BC, 0x3FFF, 0x7EE0, 0x2C84, + 0x5FFE, 0x3EBC, 0x0321, 0x0000, + 0x63FF, 0x36DC, 0x11F6, 0x392A, + 0x65EF, 0x7DBF, 0x035F, 0x2108, + 0x2B6C, 0x7FFF, 0x1CD9, 0x0007, + 0x53FC, 0x1F2F, 0x0E29, 0x0061, + 0x36BE, 0x7EAF, 0x681A, 0x3C00, + 0x7BBE, 0x329D, 0x1DE8, 0x0423, + 0x739F, 0x6A9B, 0x7293, 0x0001, + 0x5FFF, 0x6732, 0x3DA9, 0x2481, + 0x577F, 0x3EBC, 0x456F, 0x1880, + 0x6B57, 0x6E1B, 0x5010, 0x0007, + 0x0F96, 0x2C97, 0x0045, 0x3200, + 0x67FF, 0x2F17, 0x2230, 0x1548, +}; + +static const struct { + char name[16]; + unsigned palette_index; +} palette_assignments[] = +{ + {"ZELDA", 5}, + {"SUPER MARIOLAND", 6}, + {"MARIOLAND2", 0x14}, + {"SUPERMARIOLAND3", 2}, + {"KIRBY DREAM LAND", 0xB}, + {"HOSHINOKA-BI", 0xB}, + {"KIRBY'S PINBALL", 3}, + {"YOSSY NO TAMAGO", 0xC}, + {"MARIO & YOSHI", 0xC}, + {"YOSSY NO COOKIE", 4}, + {"YOSHI'S COOKIE", 4}, + {"DR.MARIO", 0x12}, + {"TETRIS", 0x11}, + {"YAKUMAN", 0x13}, + {"METROID2", 0x1F}, + {"KAERUNOTAMENI", 9}, + {"GOLF", 0x18}, + {"ALLEY WAY", 0x16}, + {"BASEBALL", 0xF}, + {"TENNIS", 0x17}, + {"F1RACE", 0x1E}, + {"KID ICARUS", 0xE}, + {"QIX", 0x19}, + {"SOLARSTRIKER", 7}, + {"X", 0x1C}, + {"GBWARS", 0x15}, +}; + +static void command_ready(GB_gameboy_t *gb) +{ + /* SGB header commands are used to send the contents of the header to the SNES CPU. + A header command looks like this: + Command ID: 0b1111xxx1, where xxx is the packet index. (e.g. F1 for [0x104, 0x112), F3 for [0x112, 0x120)) + Checksum: Simple one byte sum for the following content bytes + 0xE content bytes. The last command, FB, is padded with zeros, so information past the header is not sent. */ + + if ((gb->sgb->command[0] & 0xF1) == 0xF1) { + if (gb->boot_rom_finished) return; + + uint8_t checksum = 0; + for (unsigned i = 2; i < 0x10; i++) { + checksum += gb->sgb->command[i]; + } + if (checksum != gb->sgb->command[1]) { + GB_log(gb, "Failed checksum for SGB header command, disabling SGB features\n"); + gb->sgb->disable_commands = true; + return; + } + unsigned index = (gb->sgb->command[0] >> 1) & 7; + if (index > 5) { + return; + } + memcpy(&gb->sgb->received_header[index * 14], &gb->sgb->command[2], 14); + if (gb->sgb->command[0] == 0xfb) { + if (gb->sgb->received_header[0x42] != 3 || gb->sgb->received_header[0x47] != 0x33) { + gb->sgb->disable_commands = true; + for (unsigned i = 0; i < sizeof(palette_assignments) / sizeof(palette_assignments[0]); i++) { + if (memcmp(palette_assignments[i].name, &gb->sgb->received_header[0x30], sizeof(palette_assignments[i].name)) == 0) { + gb->sgb->effective_palettes[0] = built_in_palettes[palette_assignments[i].palette_index * 4 - 4]; + gb->sgb->effective_palettes[1] = built_in_palettes[palette_assignments[i].palette_index * 4 + 1 - 4]; + gb->sgb->effective_palettes[2] = built_in_palettes[palette_assignments[i].palette_index * 4 + 2 - 4]; + gb->sgb->effective_palettes[3] = built_in_palettes[palette_assignments[i].palette_index * 4 + 3 - 4]; + break; + } + } + } + } + return; + } + + /* Ignore malformed commands (0 length)*/ + if ((gb->sgb->command[0] & 7) == 0) return; + + switch (gb->sgb->command[0] >> 3) { + case PAL01: + pal_command(gb, 0, 1); + break; + case PAL23: + pal_command(gb, 2, 3); + break; + case PAL03: + pal_command(gb, 0, 3); + break; + case PAL12: + pal_command(gb, 1, 2); + break; + case ATTR_BLK: { + struct { + uint8_t count; + struct { + uint8_t control; + uint8_t palettes; + uint8_t left, top, right, bottom; + } data[]; + } *command = (void *)(gb->sgb->command + 1); + if (command->count > 0x12) return; + + for (unsigned i = 0; i < command->count; i++) { + bool inside = command->data[i].control & 1; + bool middle = command->data[i].control & 2; + bool outside = command->data[i].control & 4; + uint8_t inside_palette = command->data[i].palettes & 0x3; + uint8_t middle_palette = (command->data[i].palettes >> 2) & 0x3; + uint8_t outside_palette = (command->data[i].palettes >> 4) & 0x3; + + if (inside && !middle && !outside) { + middle = true; + middle_palette = inside_palette; + } + else if (outside && !middle && !inside) { + middle = true; + middle_palette = outside_palette; + } + + command->data[i].left &= 0x1F; + command->data[i].top &= 0x1F; + command->data[i].right &= 0x1F; + command->data[i].bottom &= 0x1F; + + for (unsigned y = 0; y < 18; y++) { + for (unsigned x = 0; x < 20; x++) { + if (x < command->data[i].left || x > command->data[i].right || + y < command->data[i].top || y > command->data[i].bottom) { + if (outside) { + gb->sgb->attribute_map[x + 20 * y] = outside_palette; + } + } + else if (x > command->data[i].left && x < command->data[i].right && + y > command->data[i].top && y < command->data[i].bottom) { + if (inside) { + gb->sgb->attribute_map[x + 20 * y] = inside_palette; + } + } + else if (middle) { + gb->sgb->attribute_map[x + 20 * y] = middle_palette; + } + } + } + } + break; + } + case ATTR_CHR: { + struct __attribute__((packed)) { + uint8_t x, y; + uint16_t length; + uint8_t direction; + uint8_t data[]; + } *command = (void *)(gb->sgb->command + 1); + + uint16_t count = command->length; +#ifdef GB_BIG_ENDIAN + count = __builtin_bswap16(count); +#endif + uint8_t x = command->x; + uint8_t y = command->y; + if (x >= 20 || y >= 18 || (count + 3) / 4 > sizeof(gb->sgb->command) - sizeof(*command) - 1) { + /* TODO: Verify with the SFC BIOS */ + break; + } + + for (unsigned i = 0; i < count; i++) { + uint8_t palette = (command->data[i / 4] >> (((~i) & 3) << 1)) & 3; + gb->sgb->attribute_map[x + 20 * y] = palette; + if (command->direction) { + y++; + if (y == 18) { + x++; + y = 0; + if (x == 20) { + x = 0; + } + } + } + else { + x++; + if (x == 20) { + y++; + x = 0; + if (y == 18) { + y = 0; + } + } + } + } + + break; + } + case ATTR_LIN: { + struct { + uint8_t count; + uint8_t data[]; + } *command = (void *)(gb->sgb->command + 1); + if (command->count > sizeof(gb->sgb->command) - 2) return; + + for (unsigned i = 0; i < command->count; i++) { + bool horizontal = command->data[i] & 0x80; + uint8_t palette = (command->data[i] >> 5) & 0x3; + uint8_t line = (command->data[i]) & 0x1F; + + if (horizontal) { + if (line > 18) continue; + for (unsigned x = 0; x < 20; x++) { + gb->sgb->attribute_map[x + 20 * line] = palette; + } + } + else { + if (line > 20) continue; + for (unsigned y = 0; y < 18; y++) { + gb->sgb->attribute_map[line + 20 * y] = palette; + } + } + } + break; + } + case ATTR_DIV: { + uint8_t high_palette = gb->sgb->command[1] & 3; + uint8_t low_palette = (gb->sgb->command[1] >> 2) & 3; + uint8_t middle_palette = (gb->sgb->command[1] >> 4) & 3; + bool horizontal = gb->sgb->command[1] & 0x40; + uint8_t line = gb->sgb->command[2] & 0x1F; + + for (unsigned y = 0; y < 18; y++) { + for (unsigned x = 0; x < 20; x++) { + if ((horizontal? y : x) < line) { + gb->sgb->attribute_map[x + 20 * y] = low_palette; + } + else if ((horizontal? y : x) == line) { + gb->sgb->attribute_map[x + 20 * y] = middle_palette; + } + else { + gb->sgb->attribute_map[x + 20 * y] = high_palette; + } + } + } + + break; + } + case PAL_SET: + memcpy(&gb->sgb->effective_palettes[0], + &gb->sgb->ram_palettes[4 * (gb->sgb->command[1] + (gb->sgb->command[2] & 1) * 0x100)], + 8); + memcpy(&gb->sgb->effective_palettes[4], + &gb->sgb->ram_palettes[4 * (gb->sgb->command[3] + (gb->sgb->command[4] & 1) * 0x100)], + 8); + memcpy(&gb->sgb->effective_palettes[8], + &gb->sgb->ram_palettes[4 * (gb->sgb->command[5] + (gb->sgb->command[6] & 1) * 0x100)], + 8); + memcpy(&gb->sgb->effective_palettes[12], + &gb->sgb->ram_palettes[4 * (gb->sgb->command[7] + (gb->sgb->command[8] & 1) * 0x100)], + 8); + + gb->sgb->effective_palettes[12] = gb->sgb->effective_palettes[8] = + gb->sgb->effective_palettes[4] = gb->sgb->effective_palettes[0]; + + if (gb->sgb->command[9] & 0x80) { + load_attribute_file(gb, gb->sgb->command[9] & 0x3F); + } + + if (gb->sgb->command[9] & 0x40) { + gb->sgb->mask_mode = MASK_DISABLED; + } + break; + case PAL_TRN: + gb->sgb->vram_transfer_countdown = 2; + gb->sgb->transfer_dest = TRANSFER_PALETTES; + break; + case DATA_SND: + // Not supported, but used by almost all SGB games for hot patching, so let's mute the warning for this + break; + case MLT_REQ: + if (gb->sgb->player_count == 1) { + gb->sgb->current_player = 0; + } + gb->sgb->player_count = (gb->sgb->command[1] & 3) + 1; /* Todo: When breaking save state comaptibility, + fix this to be 0 based. */ + if (gb->sgb->player_count == 3) { + gb->sgb->current_player++; + } + gb->sgb->mlt_lock = true; + break; + case CHR_TRN: + gb->sgb->vram_transfer_countdown = 2; + gb->sgb->transfer_dest = (gb->sgb->command[1] & 1)? TRANSFER_HIGH_TILES : TRANSFER_LOW_TILES; + break; + case PCT_TRN: + gb->sgb->vram_transfer_countdown = 2; + gb->sgb->transfer_dest = TRANSFER_BORDER_DATA; + break; + case ATTR_TRN: + gb->sgb->vram_transfer_countdown = 2; + gb->sgb->transfer_dest = TRANSFER_ATTRIBUTES; + break; + case ATTR_SET: + load_attribute_file(gb, gb->sgb->command[0] & 0x3F); + + if (gb->sgb->command[0] & 0x40) { + gb->sgb->mask_mode = MASK_DISABLED; + } + break; + case MASK_EN: + gb->sgb->mask_mode = gb->sgb->command[1] & 3; + break; + default: + if ((gb->sgb->command[0] >> 3) == 8 && + (gb->sgb->command[1] & ~0x80) == 0 && + (gb->sgb->command[2] & ~0x80) == 0) { + /* Mute/dummy sound commands, ignore this command as it's used by many games at startup */ + break; + } + GB_log(gb, "Unimplemented SGB command %x: ", gb->sgb->command[0] >> 3); + for (unsigned i = 0; i < gb->sgb->command_write_index / 8; i++) { + GB_log(gb, "%02x ", gb->sgb->command[i]); + } + GB_log(gb, "\n"); + } +} + +void GB_sgb_write(GB_gameboy_t *gb, uint8_t value) +{ + if (!GB_is_sgb(gb)) return; + if (!GB_is_hle_sgb(gb)) { + /* Notify via callback */ + return; + } + if (gb->sgb->disable_commands) return; + if (gb->sgb->command_write_index >= sizeof(gb->sgb->command) * 8) { + return; + } + + uint16_t command_size = (gb->sgb->command[0] & 7 ?: 1) * SGB_PACKET_SIZE * 8; + if ((gb->sgb->command[0] & 0xF1) == 0xF1) { + command_size = SGB_PACKET_SIZE * 8; + } + + if ((value & 0x20) == 0 && (gb->io_registers[GB_IO_JOYP] & 0x20) != 0) { + gb->sgb->mlt_lock ^= true; + } + + switch ((value >> 4) & 3) { + case 3: + gb->sgb->ready_for_pulse = true; + if ((gb->sgb->player_count & 1) == 0 && !gb->sgb->mlt_lock) { + gb->sgb->current_player++; + gb->sgb->current_player &= 3; + gb->sgb->mlt_lock = true; + } + break; + + case 2: // Zero + if (!gb->sgb->ready_for_pulse || !gb->sgb->ready_for_write) return; + if (gb->sgb->ready_for_stop) { + if (gb->sgb->command_write_index == command_size) { + command_ready(gb); + gb->sgb->command_write_index = 0; + memset(gb->sgb->command, 0, sizeof(gb->sgb->command)); + } + gb->sgb->ready_for_pulse = false; + gb->sgb->ready_for_write = false; + gb->sgb->ready_for_stop = false; + } + else { + gb->sgb->command_write_index++; + gb->sgb->ready_for_pulse = false; + if (((gb->sgb->command_write_index) & (SGB_PACKET_SIZE * 8 - 1)) == 0) { + gb->sgb->ready_for_stop = true; + } + } + break; + case 1: // One + if (!gb->sgb->ready_for_pulse || !gb->sgb->ready_for_write) return; + if (gb->sgb->ready_for_stop) { + GB_log(gb, "Corrupt SGB command.\n"); + gb->sgb->ready_for_pulse = false; + gb->sgb->ready_for_write = false; + gb->sgb->command_write_index = 0; + memset(gb->sgb->command, 0, sizeof(gb->sgb->command)); + } + else { + gb->sgb->command[gb->sgb->command_write_index / 8] |= 1 << (gb->sgb->command_write_index & 7); + gb->sgb->command_write_index++; + gb->sgb->ready_for_pulse = false; + if (((gb->sgb->command_write_index) & (SGB_PACKET_SIZE * 8 - 1)) == 0) { + gb->sgb->ready_for_stop = true; + } + } + break; + + case 0: + if (!gb->sgb->ready_for_pulse) return; + gb->sgb->ready_for_write = true; + gb->sgb->ready_for_pulse = false; + if (((gb->sgb->command_write_index) & (SGB_PACKET_SIZE * 8 - 1)) != 0 || + gb->sgb->command_write_index == 0 || + gb->sgb->ready_for_stop) { + gb->sgb->command_write_index = 0; + memset(gb->sgb->command, 0, sizeof(gb->sgb->command)); + gb->sgb->ready_for_stop = false; + } + break; + + default: + break; + } +} + +static uint32_t convert_rgb15(GB_gameboy_t *gb, uint16_t color) +{ + return GB_convert_rgb15(gb, color, false); +} + +static uint32_t convert_rgb15_with_fade(GB_gameboy_t *gb, uint16_t color, uint8_t fade) +{ + uint8_t r = ((color) & 0x1F) - fade; + uint8_t g = ((color >> 5) & 0x1F) - fade; + uint8_t b = ((color >> 10) & 0x1F) - fade; + + if (r >= 0x20) r = 0; + if (g >= 0x20) g = 0; + if (b >= 0x20) b = 0; + + color = r | (g << 5) | (b << 10); + + return GB_convert_rgb15(gb, color, false); +} + +#include +static void render_boot_animation (GB_gameboy_t *gb) +{ +#include "graphics/sgb_animation_logo.inc" + uint32_t *output = gb->screen; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 48 + 40 * 256; + } + uint8_t *input = animation_logo; + unsigned fade_blue = 0; + unsigned fade_red = 0; + if (gb->sgb->intro_animation < 80 - 32) { + fade_blue = 32; + } + else if (gb->sgb->intro_animation < 80) { + fade_blue = 80 - gb->sgb->intro_animation; + } + else if (gb->sgb->intro_animation > INTRO_ANIMATION_LENGTH - 32) { + fade_red = fade_blue = gb->sgb->intro_animation - INTRO_ANIMATION_LENGTH + 32; + } + uint32_t colors[] = { + convert_rgb15(gb, 0), + convert_rgb15_with_fade(gb, 0x14A5, fade_blue), + convert_rgb15_with_fade(gb, 0x54E0, fade_blue), + convert_rgb15_with_fade(gb, 0x0019, fade_red), + convert_rgb15(gb, 0x0011), + convert_rgb15(gb, 0x0009), + }; + unsigned y_min = (144 - animation_logo_height) / 2; + unsigned y_max = y_min + animation_logo_height; + for (unsigned y = 0; y < 144; y++) { + for (unsigned x = 0; x < 160; x++) { + if (y < y_min || y >= y_max) { + *(output++) = colors[0]; + } + else { + uint8_t color = *input; + if (color >= 3) { + if (color == gb->sgb->intro_animation / 2 - 3) { + color = 5; + } + else if (color == gb->sgb->intro_animation / 2 - 4) { + color = 4; + } + else if (color < gb->sgb->intro_animation / 2 - 4) { + color = 3; + } + else { + color = 0; + } + } + *(output++) = colors[color]; + input++; + } + } + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } + } +} + +static void render_jingle(GB_gameboy_t *gb, size_t count); +void GB_sgb_render(GB_gameboy_t *gb) +{ + if (gb->apu_output.sample_rate) { + render_jingle(gb, gb->apu_output.sample_rate / GB_get_usual_frame_rate(gb)); + } + + if (gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) gb->sgb->intro_animation++; + + if (gb->sgb->vram_transfer_countdown) { + if (--gb->sgb->vram_transfer_countdown == 0) { + if (gb->sgb->transfer_dest == TRANSFER_LOW_TILES || gb->sgb->transfer_dest == TRANSFER_HIGH_TILES) { + uint8_t *base = &gb->sgb->pending_border.tiles[gb->sgb->transfer_dest == TRANSFER_HIGH_TILES ? 0x80 * 8 * 8 : 0]; + for (unsigned tile = 0; tile < 0x80; tile++) { + unsigned tile_x = (tile % 10) * 16; + unsigned tile_y = (tile / 10) * 8; + for (unsigned y = 0; y < 0x8; y++) { + for (unsigned x = 0; x < 0x8; x++) { + base[tile * 8 * 8 + y * 8 + x] = gb->sgb->screen_buffer[(tile_x + x) + (tile_y + y) * 160] + + gb->sgb->screen_buffer[(tile_x + x + 8) + (tile_y + y) * 160] * 4; + } + } + } + + } + else { + unsigned size = 0; + uint16_t *data = NULL; + + switch (gb->sgb->transfer_dest) { + case TRANSFER_PALETTES: + size = 0x100; + data = gb->sgb->ram_palettes; + break; + case TRANSFER_BORDER_DATA: + size = 0x88; + data = gb->sgb->pending_border.raw_data; + break; + case TRANSFER_ATTRIBUTES: + size = 0xFE; + data = (uint16_t *)gb->sgb->attribute_files; + break; + default: + return; // Corrupt state? + } + + for (unsigned tile = 0; tile < size; tile++) { + unsigned tile_x = (tile % 20) * 8; + unsigned tile_y = (tile / 20) * 8; + for (unsigned y = 0; y < 0x8; y++) { + static const uint16_t pixel_to_bits[4] = {0x0000, 0x0080, 0x8000, 0x8080}; + *data = 0; + for (unsigned x = 0; x < 8; x++) { + *data |= pixel_to_bits[gb->sgb->screen_buffer[(tile_x + x) + (tile_y + y) * 160] & 3] >> x; + } +#ifdef GB_BIG_ENDIAN + if (gb->sgb->transfer_dest == TRANSFER_ATTRIBUTES) { + *data = __builtin_bswap16(*data); + } +#endif + data++; + } + } + if (gb->sgb->transfer_dest == TRANSFER_BORDER_DATA) { + gb->sgb->border_animation = 64; + } + } + } + } + + if (!gb->screen || !gb->rgb_encode_callback || gb->disable_rendering) return; + + uint32_t colors[4 * 4]; + for (unsigned i = 0; i < 4 * 4; i++) { + colors[i] = convert_rgb15(gb, gb->sgb->effective_palettes[i]); + } + + if (gb->sgb->mask_mode != MASK_FREEZE) { + memcpy(gb->sgb->effective_screen_buffer, + gb->sgb->screen_buffer, + sizeof(gb->sgb->effective_screen_buffer)); + } + + if (gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) { + render_boot_animation(gb); + } + else { + uint32_t *output = gb->screen; + if (gb->border_mode != GB_BORDER_NEVER) { + output += 48 + 40 * 256; + } + uint8_t *input = gb->sgb->effective_screen_buffer; + switch ((mask_mode_t) gb->sgb->mask_mode) { + case MASK_DISABLED: + case MASK_FREEZE: { + for (unsigned y = 0; y < 144; y++) { + for (unsigned x = 0; x < 160; x++) { + uint8_t palette = gb->sgb->attribute_map[x / 8 + y / 8 * 20] & 3; + *(output++) = colors[(*(input++) & 3) + palette * 4]; + } + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } + } + break; + } + case MASK_BLACK: + { + uint32_t black = convert_rgb15(gb, 0); + for (unsigned y = 0; y < 144; y++) { + for (unsigned x = 0; x < 160; x++) { + *(output++) = black; + } + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } + } + break; + } + case MASK_COLOR_0: + { + for (unsigned y = 0; y < 144; y++) { + for (unsigned x = 0; x < 160; x++) { + *(output++) = colors[0]; + } + if (gb->border_mode != GB_BORDER_NEVER) { + output += 256 - 160; + } + } + break; + } + } + } + + uint32_t border_colors[16 * 4]; + if (gb->sgb->border_animation == 0 || gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) { + for (unsigned i = 0; i < 16 * 4; i++) { + border_colors[i] = convert_rgb15(gb, gb->sgb->border.palette[i]); + } + } + else if (gb->sgb->border_animation > 32) { + gb->sgb->border_animation--; + for (unsigned i = 0; i < 16 * 4; i++) { + border_colors[i] = convert_rgb15_with_fade(gb, gb->sgb->border.palette[i], 64 - gb->sgb->border_animation); + } + } + else { + gb->sgb->border_animation--; + for (unsigned i = 0; i < 16 * 4; i++) { + border_colors[i] = convert_rgb15_with_fade(gb, gb->sgb->border.palette[i], gb->sgb->border_animation); + } + } + + + if (gb->sgb->border_animation == 32) { + memcpy(&gb->sgb->border, &gb->sgb->pending_border, sizeof(gb->sgb->border)); + } + + for (unsigned tile_y = 0; tile_y < 28; tile_y++) { + for (unsigned tile_x = 0; tile_x < 32; tile_x++) { + bool gb_area = false; + if (tile_x >= 6 && tile_x < 26 && tile_y >= 5 && tile_y < 23) { + gb_area = true; + } + else if (gb->border_mode == GB_BORDER_NEVER) { + continue; + } + uint16_t tile = gb->sgb->border.map[tile_x + tile_y * 32]; + uint8_t flip_x = (tile & 0x4000)? 0x7 : 0; + uint8_t flip_y = (tile & 0x8000)? 0x7 : 0; + uint8_t palette = (tile >> 10) & 3; + for (unsigned y = 0; y < 8; y++) { + for (unsigned x = 0; x < 8; x++) { + uint8_t color = gb->sgb->border.tiles[(tile & 0xFF) * 64 + (x ^ flip_x) + (y ^ flip_y) * 8] & 0xF; + uint32_t *output = gb->screen; + if (gb->border_mode == GB_BORDER_NEVER) { + output += (tile_x - 6) * 8 + x + ((tile_y - 5) * 8 + y) * 160; + } + else { + output += tile_x * 8 + x + (tile_y * 8 + y) * 256; + } + if (color == 0) { + if (gb_area) continue; + *output = colors[0]; + } + else { + *output = border_colors[color + palette * 16]; + } + } + } + } + } +} + +void GB_sgb_load_default_data(GB_gameboy_t *gb) +{ + +#include "graphics/sgb_border.inc" + + memcpy(gb->sgb->border.map, tilemap, sizeof(tilemap)); + memcpy(gb->sgb->border.palette, palette, sizeof(palette)); + + /* Expand tileset */ + for (unsigned tile = 0; tile < sizeof(tiles) / 32; tile++) { + for (unsigned y = 0; y < 8; y++) { + for (unsigned x = 0; x < 8; x++) { + gb->sgb->border.tiles[tile * 8 * 8 + y * 8 + x] = + (tiles[tile * 32 + y * 2 + 0] & (1 << (7 ^ x)) ? 1 : 0) | + (tiles[tile * 32 + y * 2 + 1] & (1 << (7 ^ x)) ? 2 : 0) | + (tiles[tile * 32 + y * 2 + 16] & (1 << (7 ^ x)) ? 4 : 0) | + (tiles[tile * 32 + y * 2 + 17] & (1 << (7 ^ x)) ? 8 : 0); + } + } + } + + if (gb->model != GB_MODEL_SGB2) { + /* Delete the "2" */ + gb->sgb->border.map[25 * 32 + 25] = gb->sgb->border.map[25 * 32 + 26] = + gb->sgb->border.map[26 * 32 + 25] = gb->sgb->border.map[26 * 32 + 26] = + gb->sgb->border.map[27 * 32 + 25] = gb->sgb->border.map[27 * 32 + 26] = + gb->sgb->border.map[0]; + + /* Re-center */ + memmove(&gb->sgb->border.map[25 * 32 + 1], &gb->sgb->border.map[25 * 32], (32 * 3 - 1) * sizeof(gb->sgb->border.map[0])); + } + gb->sgb->effective_palettes[0] = built_in_palettes[0]; + gb->sgb->effective_palettes[1] = built_in_palettes[1]; + gb->sgb->effective_palettes[2] = built_in_palettes[2]; + gb->sgb->effective_palettes[3] = built_in_palettes[3]; +} + +static double fm_synth(double phase) +{ + return (sin(phase * M_PI * 2) + + sin(phase * M_PI * 2 + sin(phase * M_PI * 2)) + + sin(phase * M_PI * 2 + sin(phase * M_PI * 3)) + + sin(phase * M_PI * 2 + sin(phase * M_PI * 4))) / 4; +} + +static double fm_sweep(double phase) +{ + double ret = 0; + for (unsigned i = 0; i < 8; i++) { + ret += sin((phase * M_PI * 2 + sin(phase * M_PI * 8) / 4) * pow(1.25, i)) * (8 - i) / 36; + } + return ret; +} +static double random_double(void) +{ + return ((signed)(GB_random32() % 0x10001) - 0x8000) / (double) 0x8000; +} + +static void render_jingle(GB_gameboy_t *gb, size_t count) +{ + const double frequencies[7] = { + 466.16, // Bb4 + 587.33, // D5 + 698.46, // F5 + 830.61, // Ab5 + 1046.50, // C6 + 1244.51, // Eb6 + 1567.98, // G6 + }; + + assert(gb->apu_output.sample_callback); + + if (gb->sgb->intro_animation < 0) { + GB_sample_t sample = {0, 0}; + for (unsigned i = 0; i < count; i++) { + gb->apu_output.sample_callback(gb, &sample); + } + return; + } + + if (gb->sgb->intro_animation >= INTRO_ANIMATION_LENGTH) return; + + signed jingle_stage = (gb->sgb->intro_animation - 64) / 3; + double sweep_cutoff_ratio = 2000.0 * pow(2, gb->sgb->intro_animation / 20.0) / gb->apu_output.sample_rate; + double sweep_phase_shift = 1000.0 * pow(2, gb->sgb->intro_animation / 40.0) / gb->apu_output.sample_rate; + if (sweep_cutoff_ratio > 1) { + sweep_cutoff_ratio = 1; + } + + GB_sample_t stereo; + for (unsigned i = 0; i < count; i++) { + double sample = 0; + for (signed f = 0; f < 7 && f < jingle_stage; f++) { + sample += fm_synth(gb->sgb_intro_jingle_phases[f]) * + (0.75 * pow(0.5, jingle_stage - f) + 0.25) / 5.0; + gb->sgb_intro_jingle_phases[f] += frequencies[f] / gb->apu_output.sample_rate; + } + if (gb->sgb->intro_animation > 100) { + sample *= pow((INTRO_ANIMATION_LENGTH - gb->sgb->intro_animation) / (INTRO_ANIMATION_LENGTH - 100.0), 3); + } + + if (gb->sgb->intro_animation < 120) { + double next = fm_sweep(gb->sgb_intro_sweep_phase) * 0.3 + random_double() * 0.7; + gb->sgb_intro_sweep_phase += sweep_phase_shift; + + gb->sgb_intro_sweep_previous_sample = next * (sweep_cutoff_ratio) + + gb->sgb_intro_sweep_previous_sample * (1 - sweep_cutoff_ratio); + sample += gb->sgb_intro_sweep_previous_sample * pow((120 - gb->sgb->intro_animation) / 120.0, 2) * 0.8; + } + + stereo.left = stereo.right = sample * 0x7000; + gb->apu_output.sample_callback(gb, &stereo); + } + + return; +} + diff --git a/bsnes/gb/Core/sgb.h b/bsnes/gb/Core/sgb.h new file mode 100644 index 00000000..aae9f755 --- /dev/null +++ b/bsnes/gb/Core/sgb.h @@ -0,0 +1,67 @@ +#ifndef sgb_h +#define sgb_h +#include "gb_struct_def.h" +#include +#include + +typedef struct GB_sgb_s GB_sgb_t; +typedef struct { + uint8_t tiles[0x100 * 8 * 8]; /* High nibble not used*/ + union { + struct { + uint16_t map[32 * 32]; + uint16_t palette[16 * 4]; + }; + uint16_t raw_data[0x440]; + }; +} GB_sgb_border_t; + +#ifdef GB_INTERNAL +struct GB_sgb_s { + uint8_t command[16 * 7]; + uint16_t command_write_index; + bool ready_for_pulse; + bool ready_for_write; + bool ready_for_stop; + bool disable_commands; + + /* Screen buffer */ + uint8_t screen_buffer[160 * 144]; // Live image from the Game Boy + uint8_t effective_screen_buffer[160 * 144]; // Image actually rendered to the screen + + /* Multiplayer Input */ + uint8_t player_count, current_player; + + /* Mask */ + uint8_t mask_mode; + + /* Data Transfer */ + uint8_t vram_transfer_countdown, transfer_dest; + + /* Border */ + GB_sgb_border_t border, pending_border; + uint8_t border_animation; + + /* Colorization */ + uint16_t effective_palettes[4 * 4]; + uint16_t ram_palettes[4 * 512]; + uint8_t attribute_map[20 * 18]; + uint8_t attribute_files[0xFE0]; + + /* Intro */ + int16_t intro_animation; + + /* GB Header */ + uint8_t received_header[0x54]; + + /* Multiplayer (cont) */ + bool mlt_lock; +}; + +void GB_sgb_write(GB_gameboy_t *gb, uint8_t value); +void GB_sgb_render(GB_gameboy_t *gb); +void GB_sgb_load_default_data(GB_gameboy_t *gb); + +#endif + +#endif diff --git a/bsnes/gb/Core/sm83_cpu.c b/bsnes/gb/Core/sm83_cpu.c new file mode 100644 index 00000000..3b3ecebb --- /dev/null +++ b/bsnes/gb/Core/sm83_cpu.c @@ -0,0 +1,1590 @@ +#include +#include +#include +#include "gb.h" + + +typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode); + +typedef enum { + /* Default behavior. If the CPU writes while another component reads, it reads the old value */ + GB_CONFLICT_READ_OLD, + /* If the CPU writes while another component reads, it reads the new value */ + GB_CONFLICT_READ_NEW, + /* If the CPU and another component write at the same time, the CPU's value "wins" */ + GB_CONFLICT_WRITE_CPU, + /* Register specific values */ + GB_CONFLICT_STAT_CGB, + GB_CONFLICT_STAT_DMG, + GB_CONFLICT_PALETTE_DMG, + GB_CONFLICT_PALETTE_CGB, + GB_CONFLICT_DMG_LCDC, + GB_CONFLICT_SGB_LCDC, + GB_CONFLICT_WX, +} GB_conflict_t; + +/* Todo: How does double speed mode affect these? */ +static const GB_conflict_t cgb_conflict_map[0x80] = { + [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, + [GB_IO_LYC] = GB_CONFLICT_WRITE_CPU, + [GB_IO_STAT] = GB_CONFLICT_STAT_CGB, + [GB_IO_BGP] = GB_CONFLICT_PALETTE_CGB, + [GB_IO_OBP0] = GB_CONFLICT_PALETTE_CGB, + [GB_IO_OBP1] = GB_CONFLICT_PALETTE_CGB, + + /* Todo: most values not verified, and probably differ between revisions */ +}; + +/* Todo: verify on an MGB */ +static const GB_conflict_t dmg_conflict_map[0x80] = { + [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, + [GB_IO_LYC] = GB_CONFLICT_READ_OLD, + [GB_IO_LCDC] = GB_CONFLICT_DMG_LCDC, + [GB_IO_SCY] = GB_CONFLICT_READ_NEW, + [GB_IO_STAT] = GB_CONFLICT_STAT_DMG, + + [GB_IO_BGP] = GB_CONFLICT_PALETTE_DMG, + [GB_IO_OBP0] = GB_CONFLICT_PALETTE_DMG, + [GB_IO_OBP1] = GB_CONFLICT_PALETTE_DMG, + [GB_IO_WY] = GB_CONFLICT_READ_OLD, + [GB_IO_WX] = GB_CONFLICT_WX, + + /* Todo: these were not verified at all */ + [GB_IO_SCX] = GB_CONFLICT_READ_NEW, +}; + +/* Todo: Verify on an SGB1 */ +static const GB_conflict_t sgb_conflict_map[0x80] = { + [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, + [GB_IO_LYC] = GB_CONFLICT_READ_OLD, + [GB_IO_LCDC] = GB_CONFLICT_SGB_LCDC, + [GB_IO_SCY] = GB_CONFLICT_READ_NEW, + [GB_IO_STAT] = GB_CONFLICT_STAT_DMG, + + [GB_IO_BGP] = GB_CONFLICT_READ_NEW, + [GB_IO_OBP0] = GB_CONFLICT_READ_NEW, + [GB_IO_OBP1] = GB_CONFLICT_READ_NEW, + [GB_IO_WY] = GB_CONFLICT_READ_OLD, + [GB_IO_WX] = GB_CONFLICT_WX, + + /* Todo: these were not verified at all */ + [GB_IO_SCX] = GB_CONFLICT_READ_NEW, +}; + +static uint8_t cycle_read(GB_gameboy_t *gb, uint16_t addr) +{ + if (gb->pending_cycles) { + GB_advance_cycles(gb, gb->pending_cycles); + } + uint8_t ret = GB_read_memory(gb, addr); + gb->pending_cycles = 4; + return ret; +} + +static uint8_t cycle_read_inc_oam_bug(GB_gameboy_t *gb, uint16_t addr) +{ + if (gb->pending_cycles) { + GB_advance_cycles(gb, gb->pending_cycles); + } + GB_trigger_oam_bug_read_increase(gb, addr); /* Todo: test T-cycle timing */ + uint8_t ret = GB_read_memory(gb, addr); + gb->pending_cycles = 4; + return ret; +} + +/* A special case for IF during ISR, returns the old value of IF. */ +/* TODO: Verify the timing, it might be wrong in cases where, in the same M cycle, IF + is both read be the CPU, modified by the ISR, and modified by an actual interrupt. + If this timing proves incorrect, the ISR emulation must be updated so IF reads are + timed correctly. */ +static uint8_t cycle_write_if(GB_gameboy_t *gb, uint8_t value) +{ + assert(gb->pending_cycles); + GB_advance_cycles(gb, gb->pending_cycles); + uint8_t old = (gb->io_registers[GB_IO_IF]) & 0x1F; + GB_write_memory(gb, 0xFF00 + GB_IO_IF, value); + gb->pending_cycles = 4; + return old; +} + +static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + assert(gb->pending_cycles); + GB_conflict_t conflict = GB_CONFLICT_READ_OLD; + if ((addr & 0xFF80) == 0xFF00) { + const GB_conflict_t *map = NULL; + if (GB_is_cgb(gb)) { + map = cgb_conflict_map; + } + else if (GB_is_sgb(gb)) { + map = sgb_conflict_map; + } + else { + map = dmg_conflict_map; + } + conflict = map[addr & 0x7F]; + } + switch (conflict) { + case GB_CONFLICT_READ_OLD: + GB_advance_cycles(gb, gb->pending_cycles); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 4; + return; + + case GB_CONFLICT_READ_NEW: + GB_advance_cycles(gb, gb->pending_cycles - 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 5; + return; + + case GB_CONFLICT_WRITE_CPU: + GB_advance_cycles(gb, gb->pending_cycles + 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 3; + return; + + /* The DMG STAT-write bug is basically the STAT register being read as FF for a single T-cycle */ + case GB_CONFLICT_STAT_DMG: + GB_advance_cycles(gb, gb->pending_cycles); + /* State 7 is the edge between HBlank and OAM mode, and it behaves a bit weird. + The OAM interrupt seems to be blocked by HBlank interrupts in that case, despite + the timing not making much sense for that. + This is a hack to simulate this effect */ + if (gb->display_state == 7 && (gb->io_registers[GB_IO_STAT] & 0x28) == 0x08) { + GB_write_memory(gb, addr, ~0x20); + } + else { + GB_write_memory(gb, addr, 0xFF); + } + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 3; + return; + + case GB_CONFLICT_STAT_CGB: { + /* Todo: Verify this with SCX adjustments */ + /* The LYC bit behaves differently */ + uint8_t old_value = GB_read_memory(gb, addr); + GB_advance_cycles(gb, gb->pending_cycles); + GB_write_memory(gb, addr, (old_value & 0x40) | (value & ~0x40)); + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 3; + return; + } + + /* There is some "time travel" going on with these two values, as it appears + that there's some off-by-1-T-cycle timing issue in the PPU implementation. + + This is should be accurate for every measureable scenario, though. */ + + case GB_CONFLICT_PALETTE_DMG: { + GB_advance_cycles(gb, gb->pending_cycles - 2); + uint8_t old_value = GB_read_memory(gb, addr); + GB_write_memory(gb, addr, value | old_value); + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 5; + return; + } + + case GB_CONFLICT_PALETTE_CGB: { + GB_advance_cycles(gb, gb->pending_cycles - 2); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 6; + return; + } + + case GB_CONFLICT_DMG_LCDC: { + /* Similar to the palette registers, these interact directly with the LCD, so they appear to be affected by it. Both my DMG (B, blob) and Game Boy Light behave this way though. + + Additionally, LCDC.1 is very nasty because on the it is read both by the FIFO when popping pixels, + and the sprite-fetching state machine, and both behave differently when it comes to access conflicts. + Hacks ahead. + */ + + + + uint8_t old_value = GB_read_memory(gb, addr); + GB_advance_cycles(gb, gb->pending_cycles - 2); + + if (/* gb->model != GB_MODEL_MGB && */ gb->position_in_line == 0 && (old_value & 2) && !(value & 2)) { + old_value &= ~2; + } + + GB_write_memory(gb, addr, old_value | (value & 1)); + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 5; + return; + } + + case GB_CONFLICT_SGB_LCDC: { + /* Simplified version of the above */ + + uint8_t old_value = GB_read_memory(gb, addr); + GB_advance_cycles(gb, gb->pending_cycles - 2); + /* Hack to force aborting object fetch */ + GB_write_memory(gb, addr, value); + GB_write_memory(gb, addr, old_value); + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 5; + return; + } + + case GB_CONFLICT_WX: + GB_advance_cycles(gb, gb->pending_cycles); + GB_write_memory(gb, addr, value); + gb->wx_just_changed = true; + GB_advance_cycles(gb, 1); + gb->wx_just_changed = false; + gb->pending_cycles = 3; + return; + } +} + +static void cycle_no_access(GB_gameboy_t *gb) +{ + gb->pending_cycles += 4; +} + +static void cycle_oam_bug(GB_gameboy_t *gb, uint8_t register_id) +{ + if (GB_is_cgb(gb)) { + /* Slight optimization */ + gb->pending_cycles += 4; + return; + } + if (gb->pending_cycles) { + GB_advance_cycles(gb, gb->pending_cycles); + } + GB_trigger_oam_bug(gb, gb->registers[register_id]); /* Todo: test T-cycle timing */ + gb->pending_cycles = 4; + +} + +static void flush_pending_cycles(GB_gameboy_t *gb) +{ + if (gb->pending_cycles) { + GB_advance_cycles(gb, gb->pending_cycles); + } + gb->pending_cycles = 0; +} + +/* Todo: test if multi-byte opcodes trigger the OAM bug correctly */ + +static void ill(GB_gameboy_t *gb, uint8_t opcode) +{ + GB_log(gb, "Illegal Opcode. Halting.\n"); + gb->interrupt_enable = 0; + gb->halted = true; +} + +static void nop(GB_gameboy_t *gb, uint8_t opcode) +{ +} + +static void enter_stop_mode(GB_gameboy_t *gb) +{ + gb->stopped = true; + gb->oam_ppu_blocked = !gb->oam_read_blocked; + gb->vram_ppu_blocked = !gb->vram_read_blocked; + gb->cgb_palettes_ppu_blocked = !gb->cgb_palettes_blocked; +} + +static void leave_stop_mode(GB_gameboy_t *gb) +{ + /* The CPU takes more time to wake up then the other components */ + for (unsigned i = 0x200; i--;) { + GB_advance_cycles(gb, 0x10); + } + gb->stopped = false; + gb->oam_ppu_blocked = false; + gb->vram_ppu_blocked = false; + gb->cgb_palettes_ppu_blocked = false; +} + +static void stop(GB_gameboy_t *gb, uint8_t opcode) +{ + if (gb->io_registers[GB_IO_KEY1] & 0x1) { + flush_pending_cycles(gb); + bool needs_alignment = false; + + GB_advance_cycles(gb, 0x4); + /* Make sure we keep the CPU ticks aligned correctly when returning from double speed mode */ + if (gb->double_speed_alignment & 7) { + GB_advance_cycles(gb, 0x4); + needs_alignment = true; + } + + gb->cgb_double_speed ^= true; + gb->io_registers[GB_IO_KEY1] = 0; + + enter_stop_mode(gb); + leave_stop_mode(gb); + + if (!needs_alignment) { + GB_advance_cycles(gb, 0x4); + } + + } + else { + GB_timing_sync(gb); + if ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF) { + /* HW Bug? When STOP is executed while a button is down, the CPU halts forever + yet the other hardware keeps running. */ + gb->interrupt_enable = 0; + gb->halted = true; + } + else { + enter_stop_mode(gb); + } + } + + /* Todo: is PC being actually read? */ + gb->pc++; +} + +/* Operand naming conventions for functions: + r = 8-bit register + lr = low 8-bit register + hr = high 8-bit register + rr = 16-bit register + d8 = 8-bit imm + d16 = 16-bit imm + d.. = [..] + cc = condition code (z, nz, c, nc) + */ + +static void ld_rr_d16(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + uint16_t value; + register_id = (opcode >> 4) + 1; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + value |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; + gb->registers[register_id] = value; +} + +static void ld_drr_a(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + register_id = (opcode >> 4) + 1; + cycle_write(gb, gb->registers[register_id], gb->registers[GB_REGISTER_AF] >> 8); +} + +static void inc_rr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id = (opcode >> 4) + 1; + cycle_oam_bug(gb, register_id); + gb->registers[register_id]++; +} + +static void inc_hr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + register_id = ((opcode >> 4) + 1) & 0x03; + gb->registers[register_id] += 0x100; + gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + + if ((gb->registers[register_id] & 0x0F00) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((gb->registers[register_id] & 0xFF00) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} +static void dec_hr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + register_id = ((opcode >> 4) + 1) & 0x03; + gb->registers[register_id] -= 0x100; + gb->registers[GB_REGISTER_AF] &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + + if ((gb->registers[register_id] & 0x0F00) == 0xF00) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((gb->registers[register_id] & 0xFF00) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void ld_hr_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + register_id = ((opcode >> 4) + 1) & 0x03; + gb->registers[register_id] &= 0xFF; + gb->registers[register_id] |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; +} + +static void rlca(GB_gameboy_t *gb, uint8_t opcode) +{ + bool carry = (gb->registers[GB_REGISTER_AF] & 0x8000) != 0; + + gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] & 0xFF00) << 1; + if (carry) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG | 0x0100; + } +} + +static void rla(GB_gameboy_t *gb, uint8_t opcode) +{ + bool bit7 = (gb->registers[GB_REGISTER_AF] & 0x8000) != 0; + bool carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + + gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] & 0xFF00) << 1; + if (carry) { + gb->registers[GB_REGISTER_AF] |= 0x0100; + } + if (bit7) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode) +{ + /* Todo: Verify order is correct */ + uint16_t addr; + addr = cycle_read_inc_oam_bug(gb, gb->pc++); + addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; + cycle_write(gb, addr, gb->registers[GB_REGISTER_SP] & 0xFF); + cycle_write(gb, addr + 1, gb->registers[GB_REGISTER_SP] >> 8); +} + +static void add_hl_rr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t hl = gb->registers[GB_REGISTER_HL]; + uint16_t rr; + uint8_t register_id; + cycle_no_access(gb); + register_id = (opcode >> 4) + 1; + rr = gb->registers[register_id]; + gb->registers[GB_REGISTER_HL] = hl + rr; + gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_CARRY_FLAG | GB_HALF_CARRY_FLAG); + + /* The meaning of the Half Carry flag is really hard to track -_- */ + if (((hl & 0xFFF) + (rr & 0xFFF)) & 0x1000) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ( ((unsigned) hl + (unsigned) rr) & 0x10000) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void ld_a_drr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + register_id = (opcode >> 4) + 1; + gb->registers[GB_REGISTER_AF] &= 0xFF; + gb->registers[GB_REGISTER_AF] |= cycle_read(gb, gb->registers[register_id]) << 8; +} + +static void dec_rr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id = (opcode >> 4) + 1; + cycle_oam_bug(gb, register_id); + gb->registers[register_id]--; +} + +static void inc_lr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + uint8_t value; + register_id = (opcode >> 4) + 1; + + value = (gb->registers[register_id] & 0xFF) + 1; + gb->registers[register_id] = (gb->registers[register_id] & 0xFF00) | value; + + gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + + if ((gb->registers[register_id] & 0x0F) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((gb->registers[register_id] & 0xFF) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} +static void dec_lr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + uint8_t value; + register_id = (opcode >> 4) + 1; + + value = (gb->registers[register_id] & 0xFF) - 1; + gb->registers[register_id] = (gb->registers[register_id] & 0xFF00) | value; + + gb->registers[GB_REGISTER_AF] &= ~(GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + + if ((gb->registers[register_id] & 0x0F) == 0xF) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((gb->registers[register_id] & 0xFF) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void ld_lr_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + register_id = (opcode >> 4) + 1; + gb->registers[register_id] &= 0xFF00; + gb->registers[register_id] |= cycle_read_inc_oam_bug(gb, gb->pc++); +} + +static void rrca(GB_gameboy_t *gb, uint8_t opcode) +{ + bool carry = (gb->registers[GB_REGISTER_AF] & 0x100) != 0; + + gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] >> 1) & 0xFF00; + if (carry) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG | 0x8000; + } +} + +static void rra(GB_gameboy_t *gb, uint8_t opcode) +{ + bool bit1 = (gb->registers[GB_REGISTER_AF] & 0x0100) != 0; + bool carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + + gb->registers[GB_REGISTER_AF] = (gb->registers[GB_REGISTER_AF] >> 1) & 0xFF00; + if (carry) { + gb->registers[GB_REGISTER_AF] |= 0x8000; + } + if (bit1) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void jr_r8(GB_gameboy_t *gb, uint8_t opcode) +{ + /* Todo: Verify timing */ + gb->pc += (int8_t)cycle_read_inc_oam_bug(gb, gb->pc) + 1; + cycle_no_access(gb); +} + +static bool condition_code(GB_gameboy_t *gb, uint8_t opcode) +{ + switch ((opcode >> 3) & 0x3) { + case 0: + return !(gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + case 1: + return (gb->registers[GB_REGISTER_AF] & GB_ZERO_FLAG); + case 2: + return !(gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + case 3: + return (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG); + } + + return false; +} + +static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode) +{ + int8_t offset = cycle_read_inc_oam_bug(gb, gb->pc++); + if (condition_code(gb, opcode)) { + gb->pc += offset; + cycle_no_access(gb); + } +} + +static void daa(GB_gameboy_t *gb, uint8_t opcode) +{ + int16_t result = gb->registers[GB_REGISTER_AF] >> 8; + + gb->registers[GB_REGISTER_AF] &= ~(0xFF00 | GB_ZERO_FLAG); + + if (gb->registers[GB_REGISTER_AF] & GB_SUBTRACT_FLAG) { + if (gb->registers[GB_REGISTER_AF] & GB_HALF_CARRY_FLAG) { + result = (result - 0x06) & 0xFF; + } + + if (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) { + result -= 0x60; + } + } + else { + if ((gb->registers[GB_REGISTER_AF] & GB_HALF_CARRY_FLAG) || (result & 0x0F) > 0x09) { + result += 0x06; + } + + if ((gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) || result > 0x9F) { + result += 0x60; + } + } + + if ((result & 0xFF) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + + if ((result & 0x100) == 0x100) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + + gb->registers[GB_REGISTER_AF] &= ~GB_HALF_CARRY_FLAG; + gb->registers[GB_REGISTER_AF] |= result << 8; +} + +static void cpl(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_AF] ^= 0xFF00; + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG; +} + +static void scf(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + gb->registers[GB_REGISTER_AF] &= ~(GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG); +} + +static void ccf(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_AF] ^= GB_CARRY_FLAG; + gb->registers[GB_REGISTER_AF] &= ~(GB_HALF_CARRY_FLAG | GB_SUBTRACT_FLAG); +} + +static void ld_dhli_a(GB_gameboy_t *gb, uint8_t opcode) +{ + cycle_write(gb, gb->registers[GB_REGISTER_HL]++, gb->registers[GB_REGISTER_AF] >> 8); +} + +static void ld_dhld_a(GB_gameboy_t *gb, uint8_t opcode) +{ + cycle_write(gb, gb->registers[GB_REGISTER_HL]--, gb->registers[GB_REGISTER_AF] >> 8); +} + +static void ld_a_dhli(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_AF] &= 0xFF; + gb->registers[GB_REGISTER_AF] |= cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_HL]++) << 8; +} + +static void ld_a_dhld(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_AF] &= 0xFF; + gb->registers[GB_REGISTER_AF] |= cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_HL]--) << 8; +} + +static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value; + value = cycle_read(gb, gb->registers[GB_REGISTER_HL]) + 1; + cycle_write(gb, gb->registers[GB_REGISTER_HL], value); + + gb->registers[GB_REGISTER_AF] &= ~(GB_SUBTRACT_FLAG | GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + if ((value & 0x0F) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((value & 0xFF) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void dec_dhl(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value; + value = cycle_read(gb, gb->registers[GB_REGISTER_HL]) - 1; + cycle_write(gb, gb->registers[GB_REGISTER_HL], value); + + gb->registers[GB_REGISTER_AF] &= ~( GB_ZERO_FLAG | GB_HALF_CARRY_FLAG); + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + if ((value & 0x0F) == 0x0F) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((value & 0xFF) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t data = cycle_read_inc_oam_bug(gb, gb->pc++); + cycle_write(gb, gb->registers[GB_REGISTER_HL], data); +} + +static uint8_t get_src_value(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t src_register_id; + uint8_t src_low; + src_register_id = ((opcode >> 1) + 1) & 3; + src_low = opcode & 1; + if (src_register_id == GB_REGISTER_AF) { + if (src_low) { + return gb->registers[GB_REGISTER_AF] >> 8; + } + return cycle_read(gb, gb->registers[GB_REGISTER_HL]); + } + if (src_low) { + return gb->registers[src_register_id] & 0xFF; + } + return gb->registers[src_register_id] >> 8; +} + +static void set_src_value(GB_gameboy_t *gb, uint8_t opcode, uint8_t value) +{ + uint8_t src_register_id; + uint8_t src_low; + src_register_id = ((opcode >> 1) + 1) & 3; + src_low = opcode & 1; + + if (src_register_id == GB_REGISTER_AF) { + if (src_low) { + gb->registers[GB_REGISTER_AF] &= 0xFF; + gb->registers[GB_REGISTER_AF] |= value << 8; + } + else { + cycle_write(gb, gb->registers[GB_REGISTER_HL], value); + } + } + else { + if (src_low) { + gb->registers[src_register_id] &= 0xFF00; + gb->registers[src_register_id] |= value; + } + else { + gb->registers[src_register_id] &= 0xFF; + gb->registers[src_register_id] |= value << 8; + } + } +} + +/* The LD r,r instruction is extremely common and extremely simple. Decoding this opcode at runtime is a significent + performance hit, so we generate functions for every ld x,y couple (including [hl]) at compile time using macros. */ + +/* Todo: It's probably wise to do the same to all opcodes. */ + +#define LD_X_Y(x, y) \ +static void ld_##x##_##y(GB_gameboy_t *gb, uint8_t opcode) \ +{ \ + gb->x = gb->y;\ +} + +#define LD_X_DHL(x) \ +static void ld_##x##_##dhl(GB_gameboy_t *gb, uint8_t opcode) \ +{ \ +gb->x = cycle_read(gb, gb->registers[GB_REGISTER_HL]); \ +} + +#define LD_DHL_Y(y) \ +static void ld_##dhl##_##y(GB_gameboy_t *gb, uint8_t opcode) \ +{ \ +cycle_write(gb, gb->registers[GB_REGISTER_HL], gb->y); \ +} + +LD_X_Y(b,c) LD_X_Y(b,d) LD_X_Y(b,e) LD_X_Y(b,h) LD_X_Y(b,l) LD_X_DHL(b) LD_X_Y(b,a) +LD_X_Y(c,b) LD_X_Y(c,d) LD_X_Y(c,e) LD_X_Y(c,h) LD_X_Y(c,l) LD_X_DHL(c) LD_X_Y(c,a) +LD_X_Y(d,b) LD_X_Y(d,c) LD_X_Y(d,e) LD_X_Y(d,h) LD_X_Y(d,l) LD_X_DHL(d) LD_X_Y(d,a) +LD_X_Y(e,b) LD_X_Y(e,c) LD_X_Y(e,d) LD_X_Y(e,h) LD_X_Y(e,l) LD_X_DHL(e) LD_X_Y(e,a) +LD_X_Y(h,b) LD_X_Y(h,c) LD_X_Y(h,d) LD_X_Y(h,e) LD_X_Y(h,l) LD_X_DHL(h) LD_X_Y(h,a) +LD_X_Y(l,b) LD_X_Y(l,c) LD_X_Y(l,d) LD_X_Y(l,e) LD_X_Y(l,h) LD_X_DHL(l) LD_X_Y(l,a) +LD_DHL_Y(b) LD_DHL_Y(c) LD_DHL_Y(d) LD_DHL_Y(e) LD_DHL_Y(h) LD_DHL_Y(l) LD_DHL_Y(a) +LD_X_Y(a,b) LD_X_Y(a,c) LD_X_Y(a,d) LD_X_Y(a,e) LD_X_Y(a,h) LD_X_Y(a,l) LD_X_DHL(a) + +// fire the debugger if software breakpoints are enabled +static void ld_b_b(GB_gameboy_t *gb, uint8_t opcode) +{ + if (gb->has_software_breakpoints) { + gb->debug_stopped = true; + } +} + +static void add_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = (a + value) << 8; + if ((uint8_t)(a + value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) + (value & 0xF) > 0x0F) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (((unsigned) a) + ((unsigned) value) > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void adc_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a, carry; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + gb->registers[GB_REGISTER_AF] = (a + value + carry) << 8; + + if ((uint8_t)(a + value + carry) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) + (value & 0xF) + carry > 0x0F) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (((unsigned) a) + ((unsigned) value) + carry > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void sub_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG; + if (a == value) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) < (value & 0xF)) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (a < value) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a, carry; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; + + if ((uint8_t) (a - value - carry) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) < (value & 0xF) + carry) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (((unsigned) a) - ((unsigned) value) - carry > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void and_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG; + if ((a & value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void xor_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = (a ^ value) << 8; + if ((a ^ value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void or_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = (a | value) << 8; + if ((a | value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void cp_a_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = get_src_value(gb, opcode); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + if (a == value) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) < (value & 0xF)) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (a < value) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void halt(GB_gameboy_t *gb, uint8_t opcode) +{ + assert(gb->pending_cycles == 4); + gb->pending_cycles = 0; + GB_advance_cycles(gb, 4); + + gb->halted = true; + /* Despite what some online documentations say, the HALT bug also happens on a CGB, in both CGB and DMG modes. */ + if (((gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F) != 0)) { + if (gb->ime) { + gb->halted = false; + gb->pc--; + } + else { + gb->halted = false; + gb->halt_bug = true; + } + } + gb->just_halted = true; +} + +static void pop_rr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + register_id = ((opcode >> 4) + 1) & 3; + gb->registers[register_id] = cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_SP]++); + gb->registers[register_id] |= cycle_read(gb, gb->registers[GB_REGISTER_SP]++) << 8; + gb->registers[GB_REGISTER_AF] &= 0xFFF0; // Make sure we don't set impossible flags on F! See Blargg's PUSH AF test. +} + +static void jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++); + addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8); + if (condition_code(gb, opcode)) { + cycle_no_access(gb); + gb->pc = addr; + } +} + +static void jp_a16(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc); + addr |= (cycle_read_inc_oam_bug(gb, gb->pc + 1) << 8); + cycle_no_access(gb); + gb->pc = addr; + +} + +static void call_cc_a16(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t call_addr = gb->pc - 1; + uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++); + addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8); + if (condition_code(gb, opcode)) { + cycle_oam_bug(gb, GB_REGISTER_SP); + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8); + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF); + gb->pc = addr; + + GB_debugger_call_hook(gb, call_addr); + } +} + +static void push_rr(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t register_id; + cycle_oam_bug(gb, GB_REGISTER_SP); + register_id = ((opcode >> 4) + 1) & 3; + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->registers[register_id]) >> 8); + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->registers[register_id]) & 0xFF); +} + +static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = (a + value) << 8; + if ((uint8_t) (a + value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) + (value & 0xF) > 0x0F) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (((unsigned) a) + ((unsigned) value) > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a, carry; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + gb->registers[GB_REGISTER_AF] = (a + value + carry) << 8; + + if (gb->registers[GB_REGISTER_AF] == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) + (value & 0xF) + carry > 0x0F) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (((unsigned) a) + ((unsigned) value) + carry > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void sub_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG; + if (a == value) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) < (value & 0xF)) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (a < value) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a, carry; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; + + if ((uint8_t) (a - value - carry) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) < (value & 0xF) + carry) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (((unsigned) a) - ((unsigned) value) - carry > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void and_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG; + if ((a & value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void xor_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = (a ^ value) << 8; + if ((a ^ value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void or_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] = (a | value) << 8; + if ((a | value) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void cp_a_d8(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value, a; + value = cycle_read_inc_oam_bug(gb, gb->pc++); + a = gb->registers[GB_REGISTER_AF] >> 8; + gb->registers[GB_REGISTER_AF] &= 0xFF00; + gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; + if (a == value) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + if ((a & 0xF) < (value & 0xF)) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + if (a < value) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void rst(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t call_addr = gb->pc - 1; + cycle_oam_bug(gb, GB_REGISTER_SP); + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8); + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF); + gb->pc = opcode ^ 0xC7; + GB_debugger_call_hook(gb, call_addr); +} + +static void ret(GB_gameboy_t *gb, uint8_t opcode) +{ + GB_debugger_ret_hook(gb); + gb->pc = cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_SP]++); + gb->pc |= cycle_read(gb, gb->registers[GB_REGISTER_SP]++) << 8; + cycle_no_access(gb); +} + +static void reti(GB_gameboy_t *gb, uint8_t opcode) +{ + ret(gb, opcode); + gb->ime = true; +} + +static void ret_cc(GB_gameboy_t *gb, uint8_t opcode) +{ + if (condition_code(gb, opcode)) { + cycle_no_access(gb); + ret(gb, opcode); + } + else { + cycle_no_access(gb); + } +} + +static void call_a16(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t call_addr = gb->pc - 1; + uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++); + addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8); + cycle_oam_bug(gb, GB_REGISTER_SP); + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8); + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF); + gb->pc = addr; + GB_debugger_call_hook(gb, call_addr); +} + +static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t temp = cycle_read_inc_oam_bug(gb, gb->pc++); + cycle_write(gb, 0xFF00 + temp, gb->registers[GB_REGISTER_AF] >> 8); +} + +static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_AF] &= 0xFF; + uint8_t temp = cycle_read_inc_oam_bug(gb, gb->pc++); + gb->registers[GB_REGISTER_AF] |= cycle_read(gb, 0xFF00 + temp) << 8; +} + +static void ld_dc_a(GB_gameboy_t *gb, uint8_t opcode) +{ + cycle_write(gb, 0xFF00 + (gb->registers[GB_REGISTER_BC] & 0xFF), gb->registers[GB_REGISTER_AF] >> 8); +} + +static void ld_a_dc(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_AF] &= 0xFF; + gb->registers[GB_REGISTER_AF] |= cycle_read(gb, 0xFF00 + (gb->registers[GB_REGISTER_BC] & 0xFF)) << 8; +} + +static void add_sp_r8(GB_gameboy_t *gb, uint8_t opcode) +{ + int16_t offset; + uint16_t sp = gb->registers[GB_REGISTER_SP]; + offset = (int8_t) cycle_read_inc_oam_bug(gb, gb->pc++); + cycle_no_access(gb); + cycle_no_access(gb); + gb->registers[GB_REGISTER_SP] += offset; + + gb->registers[GB_REGISTER_AF] &= 0xFF00; + + /* A new instruction, a new meaning for Half Carry! */ + if ((sp & 0xF) + (offset & 0xF) > 0xF) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((sp & 0xFF) + (offset & 0xFF) > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void jp_hl(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->pc = gb->registers[GB_REGISTER_HL]; +} + +static void ld_da16_a(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t addr; + addr = cycle_read_inc_oam_bug(gb, gb->pc++); + addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; + cycle_write(gb, addr, gb->registers[GB_REGISTER_AF] >> 8); +} + +static void ld_a_da16(GB_gameboy_t *gb, uint8_t opcode) +{ + uint16_t addr; + gb->registers[GB_REGISTER_AF] &= 0xFF; + addr = cycle_read_inc_oam_bug(gb, gb->pc++); + addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; + gb->registers[GB_REGISTER_AF] |= cycle_read(gb, addr) << 8; +} + +static void di(GB_gameboy_t *gb, uint8_t opcode) +{ + /* DI is NOT delayed, not even on a CGB. Mooneye's di_timing-GS test fails on a CGB + for different reasons. */ + gb->ime = false; +} + +static void ei(GB_gameboy_t *gb, uint8_t opcode) +{ + /* ei is actually "disable interrupts for one instruction, then enable them". */ + if (!gb->ime && !gb->ime_toggle) { + gb->ime_toggle = true; + } +} + +static void ld_hl_sp_r8(GB_gameboy_t *gb, uint8_t opcode) +{ + int16_t offset; + gb->registers[GB_REGISTER_AF] &= 0xFF00; + offset = (int8_t) cycle_read_inc_oam_bug(gb, gb->pc++); + cycle_no_access(gb); + gb->registers[GB_REGISTER_HL] = gb->registers[GB_REGISTER_SP] + offset; + + if ((gb->registers[GB_REGISTER_SP] & 0xF) + (offset & 0xF) > 0xF) { + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + } + + if ((gb->registers[GB_REGISTER_SP] & 0xFF) + (offset & 0xFF) > 0xFF) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } +} + +static void ld_sp_hl(GB_gameboy_t *gb, uint8_t opcode) +{ + gb->registers[GB_REGISTER_SP] = gb->registers[GB_REGISTER_HL]; + cycle_no_access(gb); +} + +static void rlc_r(GB_gameboy_t *gb, uint8_t opcode) +{ + bool carry; + uint8_t value; + value = get_src_value(gb, opcode); + carry = (value & 0x80) != 0; + gb->registers[GB_REGISTER_AF] &= 0xFF00; + set_src_value(gb, opcode, (value << 1) | carry); + if (carry) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + if (!(value << 1)) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void rrc_r(GB_gameboy_t *gb, uint8_t opcode) +{ + bool carry; + uint8_t value; + value = get_src_value(gb, opcode); + carry = (value & 0x01) != 0; + gb->registers[GB_REGISTER_AF] &= 0xFF00; + value = (value >> 1) | (carry << 7); + set_src_value(gb, opcode, value); + if (carry) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + if (value == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void rl_r(GB_gameboy_t *gb, uint8_t opcode) +{ + bool carry; + uint8_t value; + bool bit7; + value = get_src_value(gb, opcode); + carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + bit7 = (value & 0x80) != 0; + + gb->registers[GB_REGISTER_AF] &= 0xFF00; + value = (value << 1) | carry; + set_src_value(gb, opcode, value); + if (bit7) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + if (value == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void rr_r(GB_gameboy_t *gb, uint8_t opcode) +{ + bool carry; + uint8_t value; + bool bit1; + + value = get_src_value(gb, opcode); + carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; + bit1 = (value & 0x1) != 0; + + gb->registers[GB_REGISTER_AF] &= 0xFF00; + value = (value >> 1) | (carry << 7); + set_src_value(gb, opcode, value); + if (bit1) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + if (value == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void sla_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value; + bool carry; + value = get_src_value(gb, opcode); + carry = (value & 0x80) != 0; + gb->registers[GB_REGISTER_AF] &= 0xFF00; + set_src_value(gb, opcode, (value << 1)); + if (carry) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + if ((value & 0x7F) == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void sra_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t bit7; + uint8_t value; + value = get_src_value(gb, opcode); + bit7 = value & 0x80; + gb->registers[GB_REGISTER_AF] &= 0xFF00; + if (value & 1) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + value = (value >> 1) | bit7; + set_src_value(gb, opcode, value); + if (value == 0) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void srl_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value; + value = get_src_value(gb, opcode); + gb->registers[GB_REGISTER_AF] &= 0xFF00; + set_src_value(gb, opcode, (value >> 1)); + if (value & 1) { + gb->registers[GB_REGISTER_AF] |= GB_CARRY_FLAG; + } + if (!(value >> 1)) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void swap_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value; + value = get_src_value(gb, opcode); + gb->registers[GB_REGISTER_AF] &= 0xFF00; + set_src_value(gb, opcode, (value >> 4) | (value << 4)); + if (!value) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } +} + +static void bit_r(GB_gameboy_t *gb, uint8_t opcode) +{ + uint8_t value; + uint8_t bit; + value = get_src_value(gb, opcode); + bit = 1 << ((opcode >> 3) & 7); + if ((opcode & 0xC0) == 0x40) { /* Bit */ + gb->registers[GB_REGISTER_AF] &= 0xFF00 | GB_CARRY_FLAG; + gb->registers[GB_REGISTER_AF] |= GB_HALF_CARRY_FLAG; + if (!(bit & value)) { + gb->registers[GB_REGISTER_AF] |= GB_ZERO_FLAG; + } + } + else if ((opcode & 0xC0) == 0x80) { /* res */ + set_src_value(gb, opcode, value & ~bit); + } + else if ((opcode & 0xC0) == 0xC0) { /* set */ + set_src_value(gb, opcode, value | bit); + } +} + +static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode) +{ + opcode = cycle_read_inc_oam_bug(gb, gb->pc++); + switch (opcode >> 3) { + case 0: + rlc_r(gb, opcode); + break; + case 1: + rrc_r(gb, opcode); + break; + case 2: + rl_r(gb, opcode); + break; + case 3: + rr_r(gb, opcode); + break; + case 4: + sla_r(gb, opcode); + break; + case 5: + sra_r(gb, opcode); + break; + case 6: + swap_r(gb, opcode); + break; + case 7: + srl_r(gb, opcode); + break; + default: + bit_r(gb, opcode); + break; + } +} + +static GB_opcode_t *opcodes[256] = { + /* X0 X1 X2 X3 X4 X5 X6 X7 */ + /* X8 X9 Xa Xb Xc Xd Xe Xf */ + nop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rlca, /* 0X */ + ld_da16_sp, add_hl_rr, ld_a_drr, dec_rr, inc_lr, dec_lr, ld_lr_d8, rrca, + stop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rla, /* 1X */ + jr_r8, add_hl_rr, ld_a_drr, dec_rr, inc_lr, dec_lr, ld_lr_d8, rra, + jr_cc_r8, ld_rr_d16, ld_dhli_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, daa, /* 2X */ + jr_cc_r8, add_hl_rr, ld_a_dhli, dec_rr, inc_lr, dec_lr, ld_lr_d8, cpl, + jr_cc_r8, ld_rr_d16, ld_dhld_a, inc_rr, inc_dhl, dec_dhl, ld_dhl_d8, scf, /* 3X */ + jr_cc_r8, add_hl_rr, ld_a_dhld, dec_rr, inc_hr, dec_hr, ld_hr_d8, ccf, + ld_b_b, ld_b_c, ld_b_d, ld_b_e, ld_b_h, ld_b_l, ld_b_dhl, ld_b_a, /* 4X */ + ld_c_b, nop, ld_c_d, ld_c_e, ld_c_h, ld_c_l, ld_c_dhl, ld_c_a, + ld_d_b, ld_d_c, nop, ld_d_e, ld_d_h, ld_d_l, ld_d_dhl, ld_d_a, /* 5X */ + ld_e_b, ld_e_c, ld_e_d, nop, ld_e_h, ld_e_l, ld_e_dhl, ld_e_a, + ld_h_b, ld_h_c, ld_h_d, ld_h_e, nop, ld_h_l, ld_h_dhl, ld_h_a, /* 6X */ + ld_l_b, ld_l_c, ld_l_d, ld_l_e, ld_l_h, nop, ld_l_dhl, ld_l_a, + ld_dhl_b, ld_dhl_c, ld_dhl_d, ld_dhl_e, ld_dhl_h, ld_dhl_l, halt, ld_dhl_a, /* 7X */ + ld_a_b, ld_a_c, ld_a_d, ld_a_e, ld_a_h, ld_a_l, ld_a_dhl, nop, + add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, /* 8X */ + adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, + sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, /* 9X */ + sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, + and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, /* aX */ + xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, + or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, /* bX */ + cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, + ret_cc, pop_rr, jp_cc_a16, jp_a16, call_cc_a16,push_rr, add_a_d8, rst, /* cX */ + ret_cc, ret, jp_cc_a16, cb_prefix, call_cc_a16,call_a16, adc_a_d8, rst, + ret_cc, pop_rr, jp_cc_a16, ill, call_cc_a16,push_rr, sub_a_d8, rst, /* dX */ + ret_cc, reti, jp_cc_a16, ill, call_cc_a16,ill, sbc_a_d8, rst, + ld_da8_a, pop_rr, ld_dc_a, ill, ill, push_rr, and_a_d8, rst, /* eX */ + add_sp_r8, jp_hl, ld_da16_a, ill, ill, ill, xor_a_d8, rst, + ld_a_da8, pop_rr, ld_a_dc, di, ill, push_rr, or_a_d8, rst, /* fX */ + ld_hl_sp_r8,ld_sp_hl, ld_a_da16, ei, ill, ill, cp_a_d8, rst, +}; +void GB_cpu_run(GB_gameboy_t *gb) +{ + if (gb->hdma_on) { + GB_advance_cycles(gb, 4); + return; + } + if (gb->stopped) { + GB_timing_sync(gb); + GB_advance_cycles(gb, 4); + if ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF) { + leave_stop_mode(gb); + GB_advance_cycles(gb, 8); + } + return; + } + + if ((gb->interrupt_enable & 0x10) && (gb->ime || gb->halted)) { + GB_timing_sync(gb); + } + + if (gb->halted && !GB_is_cgb(gb) && !gb->just_halted) { + GB_advance_cycles(gb, 2); + } + + uint8_t interrupt_queue = gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F; + + if (gb->halted) { + GB_advance_cycles(gb, (GB_is_cgb(gb) || gb->just_halted) ? 4 : 2); + } + gb->just_halted = false; + + bool effective_ime = gb->ime; + if (gb->ime_toggle) { + gb->ime = !gb->ime; + gb->ime_toggle = false; + } + + /* Wake up from HALT mode without calling interrupt code. */ + if (gb->halted && !effective_ime && interrupt_queue) { + gb->halted = false; + } + + /* Call interrupt */ + else if (effective_ime && interrupt_queue) { + gb->halted = false; + uint16_t call_addr = gb->pc; + + cycle_no_access(gb); + cycle_no_access(gb); + GB_trigger_oam_bug(gb, gb->registers[GB_REGISTER_SP]); /* Todo: test T-cycle timing */ + cycle_no_access(gb); + + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8); + interrupt_queue = gb->interrupt_enable; + + if (gb->registers[GB_REGISTER_SP] == GB_IO_IF + 0xFF00 + 1) { + gb->registers[GB_REGISTER_SP]--; + interrupt_queue &= cycle_write_if(gb, (gb->pc) & 0xFF); + } + else { + cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF); + interrupt_queue &= (gb->io_registers[GB_IO_IF]) & 0x1F; + } + + if (interrupt_queue) { + uint8_t interrupt_bit = 0; + while (!(interrupt_queue & 1)) { + interrupt_queue >>= 1; + interrupt_bit++; + } + gb->io_registers[GB_IO_IF] &= ~(1 << interrupt_bit); + gb->pc = interrupt_bit * 8 + 0x40; + } + else { + gb->pc = 0; + } + gb->ime = false; + GB_debugger_call_hook(gb, call_addr); + } + /* Run mode */ + else if (!gb->halted) { + gb->last_opcode_read = cycle_read_inc_oam_bug(gb, gb->pc++); + if (gb->halt_bug) { + gb->pc--; + gb->halt_bug = false; + } + opcodes[gb->last_opcode_read](gb, gb->last_opcode_read); + } + + flush_pending_cycles(gb); + + if (gb->hdma_starting) { + gb->hdma_starting = false; + gb->hdma_on = true; + gb->hdma_cycles = -8; + } +} diff --git a/bsnes/gb/Core/sm83_cpu.h b/bsnes/gb/Core/sm83_cpu.h new file mode 100644 index 00000000..49fa80b5 --- /dev/null +++ b/bsnes/gb/Core/sm83_cpu.h @@ -0,0 +1,11 @@ +#ifndef sm83_cpu_h +#define sm83_cpu_h +#include "gb_struct_def.h" +#include + +void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count); +#ifdef GB_INTERNAL +void GB_cpu_run(GB_gameboy_t *gb); +#endif + +#endif /* sm83_cpu_h */ diff --git a/bsnes/gb/Core/sm83_disassembler.c b/bsnes/gb/Core/sm83_disassembler.c new file mode 100644 index 00000000..7dacd9eb --- /dev/null +++ b/bsnes/gb/Core/sm83_disassembler.c @@ -0,0 +1,789 @@ +#include +#include +#include "gb.h" + + +typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc); + +static void ill(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, ".BYTE $%02x\n", opcode); + (*pc)++; +} + +static void nop(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "NOP\n"); + (*pc)++; +} + +static void stop(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint8_t next = GB_read_memory(gb, (*pc)++); + if (next) { + GB_log(gb, "CORRUPTED STOP (%02x)\n", next); + } + else { + GB_log(gb, "STOP\n"); + } +} + +static char *register_names[] = {"af", "bc", "de", "hl", "sp"}; + +static void ld_rr_d16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + uint16_t value; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + value = GB_read_memory(gb, (*pc)++); + value |= GB_read_memory(gb, (*pc)++) << 8; + const char *symbol = GB_debugger_name_for_address(gb, value); + if (symbol) { + GB_log(gb, "LD %s, %s ; =$%04x\n", register_names[register_id], symbol, value); + } + else { + GB_log(gb, "LD %s, $%04x\n", register_names[register_id], value); + } +} + +static void ld_drr_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + GB_log(gb, "LD [%s], a\n", register_names[register_id]); +} + +static void inc_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + GB_log(gb, "INC %s\n", register_names[register_id]); +} + +static void inc_hr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + (*pc)++; + register_id = ((opcode >> 4) + 1) & 0x03; + GB_log(gb, "INC %c\n", register_names[register_id][0]); + +} +static void dec_hr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + (*pc)++; + register_id = ((opcode >> 4) + 1) & 0x03; + GB_log(gb, "DEC %c\n", register_names[register_id][0]); +} + +static void ld_hr_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + (*pc)++; + register_id = ((opcode >> 4) + 1) & 0x03; + GB_log(gb, "LD %c, $%02x\n", register_names[register_id][0], GB_read_memory(gb, (*pc)++)); +} + +static void rlca(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "RLCA\n"); +} + +static void rla(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "RLA\n"); +} + +static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint16_t addr; + (*pc)++; + addr = GB_read_memory(gb, (*pc)++); + addr |= GB_read_memory(gb, (*pc)++) << 8; + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_log(gb, "LD [%s], sp ; =$%04x\n", symbol, addr); + } + else { + GB_log(gb, "LD [$%04x], sp\n", addr); + } +} + +static void add_hl_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + (*pc)++; + register_id = (opcode >> 4) + 1; + GB_log(gb, "ADD hl, %s\n", register_names[register_id]); +} + +static void ld_a_drr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + GB_log(gb, "LD a, [%s]\n", register_names[register_id]); +} + +static void dec_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + GB_log(gb, "DEC %s\n", register_names[register_id]); +} + +static void inc_lr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + + GB_log(gb, "INC %c\n", register_names[register_id][1]); +} +static void dec_lr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + + GB_log(gb, "DEC %c\n", register_names[register_id][1]); +} + +static void ld_lr_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = (GB_read_memory(gb, (*pc)++) >> 4) + 1; + + GB_log(gb, "LD %c, $%02x\n", register_names[register_id][1], GB_read_memory(gb, (*pc)++)); +} + +static void rrca(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "RRCA\n"); + (*pc)++; +} + +static void rra(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "RRA\n"); + (*pc)++; +} + +static void jr_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = *pc + (int8_t) GB_read_memory(gb, (*pc)) + 1; + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_attributed_log(gb, GB_LOG_UNDERLINE, "JR %s ; =$%04x\n", symbol, addr); + } + else { + GB_attributed_log(gb, GB_LOG_UNDERLINE, "JR $%04x\n", addr); + } + (*pc)++; +} + +static const char *condition_code(uint8_t opcode) +{ + switch ((opcode >> 3) & 0x3) { + case 0: + return "nz"; + case 1: + return "z"; + case 2: + return "nc"; + case 3: + return "c"; + } + + return NULL; +} + +static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = *pc + (int8_t) GB_read_memory(gb, (*pc)) + 1; + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_attributed_log(gb, GB_LOG_DASHED_UNDERLINE, "JR %s, %s ; =$%04x\n", condition_code(opcode), symbol, addr); + } + else { + GB_attributed_log(gb, GB_LOG_DASHED_UNDERLINE, "JR %s, $%04x\n", condition_code(opcode), addr); + } + (*pc)++; +} + +static void daa(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "DAA\n"); + (*pc)++; +} + +static void cpl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "CPL\n"); + (*pc)++; +} + +static void scf(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "SCF\n"); + (*pc)++; +} + +static void ccf(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "CCF\n"); + (*pc)++; +} + +static void ld_dhli_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "LD [hli], a\n"); + (*pc)++; +} + +static void ld_dhld_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "LD [hld], a\n"); + (*pc)++; +} + +static void ld_a_dhli(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "LD a, [hli]\n"); + (*pc)++; +} + +static void ld_a_dhld(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "LD a, [hld]\n"); + (*pc)++; +} + +static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "INC [hl]\n"); + (*pc)++; +} + +static void dec_dhl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + GB_log(gb, "DEC [hl]\n"); + (*pc)++; +} + +static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "LD [hl], $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static const char *get_src_name(uint8_t opcode) +{ + uint8_t src_register_id; + uint8_t src_low; + src_register_id = ((opcode >> 1) + 1) & 3; + src_low = (opcode & 1); + if (src_register_id == GB_REGISTER_AF) { + return src_low? "a": "[hl]"; + } + if (src_low) { + return register_names[src_register_id] + 1; + } + static const char *high_register_names[] = {"a", "b", "d", "h"}; + return high_register_names[src_register_id]; +} + +static const char *get_dst_name(uint8_t opcode) +{ + uint8_t dst_register_id; + uint8_t dst_low; + dst_register_id = ((opcode >> 4) + 1) & 3; + dst_low = opcode & 8; + if (dst_register_id == GB_REGISTER_AF) { + return dst_low? "a": "[hl]"; + } + if (dst_low) { + return register_names[dst_register_id] + 1; + } + static const char *high_register_names[] = {"a", "b", "d", "h"}; + return high_register_names[dst_register_id]; +} + +static void ld_r_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "LD %s, %s\n", get_dst_name(opcode), get_src_name(opcode)); +} + +static void add_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "ADD %s\n", get_src_name(opcode)); +} + +static void adc_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "ADC %s\n", get_src_name(opcode)); +} + +static void sub_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SUB %s\n", get_src_name(opcode)); +} + +static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SBC %s\n", get_src_name(opcode)); +} + +static void and_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "AND %s\n", get_src_name(opcode)); +} + +static void xor_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "XOR %s\n", get_src_name(opcode)); +} + +static void or_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "OR %s\n", get_src_name(opcode)); +} + +static void cp_a_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "CP %s\n", get_src_name(opcode)); +} + +static void halt(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "HALT\n"); +} + +static void ret_cc(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_attributed_log(gb, GB_LOG_DASHED_UNDERLINE, "RET %s\n", condition_code(opcode)); +} + +static void pop_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = ((GB_read_memory(gb, (*pc)++) >> 4) + 1) & 3; + GB_log(gb, "POP %s\n", register_names[register_id]); +} + +static void jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8); + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_attributed_log(gb, GB_LOG_DASHED_UNDERLINE, "JP %s, %s ; =$%04x\n", condition_code(opcode), symbol, addr); + } + else { + GB_attributed_log(gb, GB_LOG_DASHED_UNDERLINE, "JP %s, $%04x\n", condition_code(opcode), addr); + } + (*pc) += 2; +} + +static void jp_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8); + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_log(gb, "JP %s ; =$%04x\n", symbol, addr); + } + else { + GB_log(gb, "JP $%04x\n", addr); + } + (*pc) += 2; +} + +static void call_cc_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8); + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_log(gb, "CALL %s, %s ; =$%04x\n", condition_code(opcode), symbol, addr); + } + else { + GB_log(gb, "CALL %s, $%04x\n", condition_code(opcode), addr); + } + (*pc) += 2; +} + +static void push_rr(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t register_id; + register_id = ((GB_read_memory(gb, (*pc)++) >> 4) + 1) & 3; + GB_log(gb, "PUSH %s\n", register_names[register_id]); +} + +static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "ADD $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "ADC $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void sub_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SUB $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SBC $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void and_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "AND $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void xor_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "XOR $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void or_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "OR $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void cp_a_d8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "CP $%02x\n", GB_read_memory(gb, (*pc)++)); +} + +static void rst(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "RST $%02x\n", opcode ^ 0xC7); + +} + +static void ret(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_attributed_log(gb, GB_LOG_UNDERLINE, "RET\n"); +} + +static void reti(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_attributed_log(gb, GB_LOG_UNDERLINE, "RETI\n"); +} + +static void call_a16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8); + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_log(gb, "CALL %s ; =$%04x\n", symbol, addr); + } + else { + GB_log(gb, "CALL $%04x\n", addr); + } + (*pc) += 2; +} + +static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint8_t addr = GB_read_memory(gb, (*pc)++); + const char *symbol = GB_debugger_name_for_address(gb, 0xff00 + addr); + if (symbol) { + GB_log(gb, "LDH [%s & $FF], a ; =$%02x\n", symbol, addr); + } + else { + GB_log(gb, "LDH [$%02x], a\n", addr); + } +} + +static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint8_t addr = GB_read_memory(gb, (*pc)++); + const char *symbol = GB_debugger_name_for_address(gb, 0xff00 + addr); + if (symbol) { + GB_log(gb, "LDH a, [%s & $FF] ; =$%02x\n", symbol, addr); + } + else { + GB_log(gb, "LDH a, [$%02x]\n", addr); + } +} + +static void ld_dc_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "LDH [c], a\n"); +} + +static void ld_a_dc(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "LDH a, [c]\n"); +} + +static void add_sp_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + int8_t temp = GB_read_memory(gb, (*pc)++); + GB_log(gb, "ADD SP, %s$%02x\n", temp < 0? "-" : "", temp < 0? -temp : temp); +} + +static void jp_hl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "JP hl\n"); +} + +static void ld_da16_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8); + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_log(gb, "LD [%s], a ; =$%04x\n", symbol, addr); + } + else { + GB_log(gb, "LD [$%04x], a\n", addr); + } + (*pc) += 2; +} + +static void ld_a_da16(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + uint16_t addr = GB_read_memory(gb, *pc) | (GB_read_memory(gb, *pc + 1) << 8); + const char *symbol = GB_debugger_name_for_address(gb, addr); + if (symbol) { + GB_log(gb, "LD a, [%s] ; =$%04x\n", symbol, addr); + } + else { + GB_log(gb, "LD a, [$%04x]\n", addr); + } + (*pc) += 2; +} + +static void di(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "DI\n"); +} + +static void ei(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "EI\n"); +} + +static void ld_hl_sp_r8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + int8_t temp = GB_read_memory(gb, (*pc)++); + GB_log(gb, "LD hl, sp, %s$%02x\n", temp < 0? "-" : "", temp < 0? -temp : temp); +} + +static void ld_sp_hl(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "LD sp, hl\n"); +} + +static void rlc_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "RLC %s\n", get_src_name(opcode)); +} + +static void rrc_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "RRC %s\n", get_src_name(opcode)); +} + +static void rl_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "RL %s\n", get_src_name(opcode)); +} + +static void rr_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "RR %s\n", get_src_name(opcode)); +} + +static void sla_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SLA %s\n", get_src_name(opcode)); +} + +static void sra_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SRA %s\n", get_src_name(opcode)); +} + +static void srl_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SRL %s\n", get_src_name(opcode)); +} + +static void swap_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + (*pc)++; + GB_log(gb, "SWAP %s\n", get_src_name(opcode)); +} + +static void bit_r(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + uint8_t bit; + (*pc)++; + bit = ((opcode >> 3) & 7); + if ((opcode & 0xC0) == 0x40) { /* Bit */ + GB_log(gb, "BIT %s, %d\n", get_src_name(opcode), bit); + } + else if ((opcode & 0xC0) == 0x80) { /* res */ + GB_log(gb, "RES %s, %d\n", get_src_name(opcode), bit); + } + else if ((opcode & 0xC0) == 0xC0) { /* set */ + GB_log(gb, "SET %s, %d\n", get_src_name(opcode), bit); + } +} + +static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) +{ + opcode = GB_read_memory(gb, ++*pc); + switch (opcode >> 3) { + case 0: + rlc_r(gb, opcode, pc); + break; + case 1: + rrc_r(gb, opcode, pc); + break; + case 2: + rl_r(gb, opcode, pc); + break; + case 3: + rr_r(gb, opcode, pc); + break; + case 4: + sla_r(gb, opcode, pc); + break; + case 5: + sra_r(gb, opcode, pc); + break; + case 6: + swap_r(gb, opcode, pc); + break; + case 7: + srl_r(gb, opcode, pc); + break; + default: + bit_r(gb, opcode, pc); + break; + } +} + +static GB_opcode_t *opcodes[256] = { + /* X0 X1 X2 X3 X4 X5 X6 X7 */ + /* X8 X9 Xa Xb Xc Xd Xe Xf */ + nop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rlca, /* 0X */ + ld_da16_sp, add_hl_rr, ld_a_drr, dec_rr, inc_lr, dec_lr, ld_lr_d8, rrca, + stop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rla, /* 1X */ + jr_r8, add_hl_rr, ld_a_drr, dec_rr, inc_lr, dec_lr, ld_lr_d8, rra, + jr_cc_r8, ld_rr_d16, ld_dhli_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, daa, /* 2X */ + jr_cc_r8, add_hl_rr, ld_a_dhli, dec_rr, inc_lr, dec_lr, ld_lr_d8, cpl, + jr_cc_r8, ld_rr_d16, ld_dhld_a, inc_rr, inc_dhl, dec_dhl, ld_dhl_d8, scf, /* 3X */ + jr_cc_r8, add_hl_rr, ld_a_dhld, dec_rr, inc_hr, dec_hr, ld_hr_d8, ccf, + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, /* 4X */ + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, /* 5X */ + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, /* 6X */ + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, halt, ld_r_r, /* 7X */ + ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, ld_r_r, + add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, add_a_r, /* 8X */ + adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, adc_a_r, + sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, sub_a_r, /* 9X */ + sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, sbc_a_r, + and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, and_a_r, /* aX */ + xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, xor_a_r, + or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, or_a_r, /* bX */ + cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, cp_a_r, + ret_cc, pop_rr, jp_cc_a16, jp_a16, call_cc_a16,push_rr, add_a_d8, rst, /* cX */ + ret_cc, ret, jp_cc_a16, cb_prefix, call_cc_a16,call_a16, adc_a_d8, rst, + ret_cc, pop_rr, jp_cc_a16, ill, call_cc_a16,push_rr, sub_a_d8, rst, /* dX */ + ret_cc, reti, jp_cc_a16, ill, call_cc_a16,ill, sbc_a_d8, rst, + ld_da8_a, pop_rr, ld_dc_a, ill, ill, push_rr, and_a_d8, rst, /* eX */ + add_sp_r8, jp_hl, ld_da16_a, ill, ill, ill, xor_a_d8, rst, + ld_a_da8, pop_rr, ld_a_dc, di, ill, push_rr, or_a_d8, rst, /* fX */ + ld_hl_sp_r8,ld_sp_hl, ld_a_da16, ei, ill, ill, cp_a_d8, rst, +}; + + + +void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count) +{ + const GB_bank_symbol_t *function_symbol = GB_debugger_find_symbol(gb, pc); + + if (function_symbol && pc - function_symbol->addr > 0x1000) { + function_symbol = NULL; + } + + if (function_symbol && pc != function_symbol->addr) { + GB_log(gb, "%s:\n", function_symbol->name); + } + + uint16_t current_function = function_symbol? function_symbol->addr : 0; + + while (count--) { + function_symbol = GB_debugger_find_symbol(gb, pc); + if (function_symbol && function_symbol->addr == pc) { + if (current_function != function_symbol->addr) { + GB_log(gb, "\n"); + } + GB_log(gb, "%s:\n", function_symbol->name); + } + if (function_symbol) { + GB_log(gb, "%s%04x <+%03x>: ", pc == gb->pc? " ->": " ", pc, pc - function_symbol->addr); + } + else { + GB_log(gb, "%s%04x: ", pc == gb->pc? " ->": " ", pc); + } + uint8_t opcode = GB_read_memory(gb, pc); + opcodes[opcode](gb, opcode, &pc); + } +} diff --git a/bsnes/gb/Core/symbol_hash.c b/bsnes/gb/Core/symbol_hash.c new file mode 100644 index 00000000..75a7837d --- /dev/null +++ b/bsnes/gb/Core/symbol_hash.c @@ -0,0 +1,108 @@ +#include "gb.h" +#include +#include +#include +#include + +static size_t GB_map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr) +{ + if (!map->symbols) { + return 0; + } + ssize_t min = 0; + ssize_t max = map->n_symbols; + while (min < max) { + size_t pivot = (min + max) / 2; + if (map->symbols[pivot].addr == addr) return pivot; + if (map->symbols[pivot].addr > addr) { + max = pivot; + } + else { + min = pivot + 1; + } + } + return (size_t) min; +} + +GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name) +{ + size_t index = GB_map_find_symbol_index(map, addr); + + map->symbols = realloc(map->symbols, (map->n_symbols + 1) * sizeof(map->symbols[0])); + memmove(&map->symbols[index + 1], &map->symbols[index], (map->n_symbols - index) * sizeof(map->symbols[0])); + map->symbols[index].addr = addr; + map->symbols[index].name = strdup(name); + map->n_symbols++; + return &map->symbols[index]; +} + +const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr) +{ + if (!map) return NULL; + size_t index = GB_map_find_symbol_index(map, addr); + if (index < map->n_symbols && map->symbols[index].addr != addr) { + index--; + } + if (index < map->n_symbols) { + return &map->symbols[index]; + } + return NULL; +} + +GB_symbol_map_t *GB_map_alloc(void) +{ + GB_symbol_map_t *map = malloc(sizeof(*map)); + memset(map, 0, sizeof(*map)); + return map; +} + +void GB_map_free(GB_symbol_map_t *map) +{ + for (unsigned i = 0; i < map->n_symbols; i++) { + free(map->symbols[i].name); + } + + if (map->symbols) { + free(map->symbols); + } + + free(map); +} + +static unsigned hash_name(const char *name) +{ + unsigned r = 0; + while (*name) { + r <<= 1; + if (r & 0x400) { + r ^= 0x401; + } + r += (unsigned char)*(name++); + } + + return r & 0x3FF; +} + +void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *bank_symbol) +{ + unsigned hash = hash_name(bank_symbol->name); + GB_symbol_t *symbol = malloc(sizeof(*symbol)); + symbol->name = bank_symbol->name; + symbol->addr = bank_symbol->addr; + symbol->bank = bank; + symbol->next = map->buckets[hash]; + map->buckets[hash] = symbol; +} + +const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name) +{ + unsigned hash = hash_name(name); + GB_symbol_t *symbol = map->buckets[hash]; + + while (symbol) { + if (strcmp(symbol->name, name) == 0) return symbol; + symbol = symbol->next; + } + + return NULL; +} diff --git a/bsnes/gb/Core/symbol_hash.h b/bsnes/gb/Core/symbol_hash.h new file mode 100644 index 00000000..2a03c96b --- /dev/null +++ b/bsnes/gb/Core/symbol_hash.h @@ -0,0 +1,38 @@ +#ifndef symbol_hash_h +#define symbol_hash_h + +#include +#include +#include + +typedef struct { + char *name; + uint16_t addr; +} GB_bank_symbol_t; + +typedef struct GB_symbol_s { + struct GB_symbol_s *next; + const char *name; + uint16_t bank; + uint16_t addr; +} GB_symbol_t; + +typedef struct { + GB_bank_symbol_t *symbols; + size_t n_symbols; +} GB_symbol_map_t; + +typedef struct { + GB_symbol_t *buckets[0x400]; +} GB_reversed_symbol_map_t; + +#ifdef GB_INTERNAL +void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *symbol); +const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name); +GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name); +const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr); +GB_symbol_map_t *GB_map_alloc(void); +void GB_map_free(GB_symbol_map_t *map); +#endif + +#endif /* symbol_hash_h */ diff --git a/bsnes/gb/Core/timing.c b/bsnes/gb/Core/timing.c new file mode 100644 index 00000000..965ba27c --- /dev/null +++ b/bsnes/gb/Core/timing.c @@ -0,0 +1,327 @@ +#include "gb.h" +#ifdef _WIN32 +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0500 +#endif +#include +#else +#include +#endif + +static const unsigned GB_TAC_TRIGGER_BITS[] = {512, 8, 32, 128}; + +#ifndef GB_DISABLE_TIMEKEEPING +static int64_t get_nanoseconds(void) +{ +#ifndef _WIN32 + struct timeval now; + gettimeofday(&now, NULL); + return (now.tv_usec) * 1000 + now.tv_sec * 1000000000L; +#else + FILETIME time; + GetSystemTimeAsFileTime(&time); + return (((int64_t)time.dwHighDateTime << 32) | time.dwLowDateTime) * 100L; +#endif +} + +static void nsleep(uint64_t nanoseconds) +{ +#ifndef _WIN32 + struct timespec sleep = {0, nanoseconds}; + nanosleep(&sleep, NULL); +#else + HANDLE timer; + LARGE_INTEGER time; + timer = CreateWaitableTimer(NULL, true, NULL); + time.QuadPart = -(nanoseconds / 100L); + SetWaitableTimer(timer, &time, 0, NULL, NULL, false); + WaitForSingleObject(timer, INFINITE); + CloseHandle(timer); +#endif +} + +bool GB_timing_sync_turbo(GB_gameboy_t *gb) +{ + if (!gb->turbo_dont_skip) { + int64_t nanoseconds = get_nanoseconds(); + if (nanoseconds <= gb->last_sync + (1000000000LL * LCDC_PERIOD / GB_get_clock_rate(gb))) { + return true; + } + gb->last_sync = nanoseconds; + } + return false; +} + +void GB_timing_sync(GB_gameboy_t *gb) +{ + if (gb->turbo) { + gb->cycles_since_last_sync = 0; + return; + } + /* Prevent syncing if not enough time has passed.*/ + if (gb->cycles_since_last_sync < LCDC_PERIOD / 3) return; + + uint64_t target_nanoseconds = gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */ + int64_t nanoseconds = get_nanoseconds(); + int64_t time_to_sleep = target_nanoseconds + gb->last_sync - nanoseconds; + if (time_to_sleep > 0 && time_to_sleep < LCDC_PERIOD * 1000000000LL / GB_get_clock_rate(gb)) { + nsleep(time_to_sleep); + gb->last_sync += target_nanoseconds; + } + else { + gb->last_sync = nanoseconds; + } + + gb->cycles_since_last_sync = 0; + if (gb->update_input_hint_callback) { + gb->update_input_hint_callback(gb); + } +} +#else + +bool GB_timing_sync_turbo(GB_gameboy_t *gb) +{ + return false; +} + +void GB_timing_sync(GB_gameboy_t *gb) +{ +} + +#endif +static void GB_ir_run(GB_gameboy_t *gb) +{ + if (gb->ir_queue_length == 0) return; + if (gb->cycles_since_input_ir_change >= gb->ir_queue[0].delay) { + gb->cycles_since_input_ir_change -= gb->ir_queue[0].delay; + gb->infrared_input = gb->ir_queue[0].state; + gb->ir_queue_length--; + memmove(&gb->ir_queue[0], &gb->ir_queue[1], sizeof(gb->ir_queue[0]) * (gb->ir_queue_length)); + } +} + +static void advance_tima_state_machine(GB_gameboy_t *gb) +{ + if (gb->tima_reload_state == GB_TIMA_RELOADED) { + gb->tima_reload_state = GB_TIMA_RUNNING; + } + else if (gb->tima_reload_state == GB_TIMA_RELOADING) { + gb->io_registers[GB_IO_IF] |= 4; + gb->tima_reload_state = GB_TIMA_RELOADED; + } +} + +static void increase_tima(GB_gameboy_t *gb) +{ + gb->io_registers[GB_IO_TIMA]++; + if (gb->io_registers[GB_IO_TIMA] == 0) { + gb->io_registers[GB_IO_TIMA] = gb->io_registers[GB_IO_TMA]; + gb->tima_reload_state = GB_TIMA_RELOADING; + } +} + +static void GB_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value) +{ + /* TIMA increases when a specific high-bit becomes a low-bit. */ + value &= INTERNAL_DIV_CYCLES - 1; + uint32_t triggers = gb->div_counter & ~value; + if ((gb->io_registers[GB_IO_TAC] & 4) && (triggers & GB_TAC_TRIGGER_BITS[gb->io_registers[GB_IO_TAC] & 3])) { + increase_tima(gb); + } + + /* TODO: Can switching to double speed mode trigger an event? */ + if (triggers & (gb->cgb_double_speed? 0x2000 : 0x1000)) { + GB_apu_run(gb); + GB_apu_div_event(gb); + } + gb->div_counter = value; +} + +static void GB_timers_run(GB_gameboy_t *gb, uint8_t cycles) +{ + if (gb->stopped) { + gb->apu.apu_cycles += 4 << !gb->cgb_double_speed; + return; + } + + GB_STATE_MACHINE(gb, div, cycles, 1) { + GB_STATE(gb, div, 1); + GB_STATE(gb, div, 2); + GB_STATE(gb, div, 3); + } + + GB_set_internal_div_counter(gb, 0); +main: + GB_SLEEP(gb, div, 1, 3); + while (true) { + advance_tima_state_machine(gb); + GB_set_internal_div_counter(gb, gb->div_counter + 4); + gb->apu.apu_cycles += 4 << !gb->cgb_double_speed; + GB_SLEEP(gb, div, 2, 4); + } + + /* Todo: This is ugly to allow compatibility with 0.11 save states. Fix me when breaking save compatibility */ + { + div3: + /* Compensate for lack of prefetch emulation, as well as DIV's internal initial value */ + GB_set_internal_div_counter(gb, 8); + goto main; + } +} + +static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) +{ + if (gb->serial_length == 0) { + gb->serial_cycles += cycles; + return; + } + + while (cycles > gb->serial_length) { + advance_serial(gb, gb->serial_length); + cycles -= gb->serial_length; + } + + uint16_t previous_serial_cycles = gb->serial_cycles; + gb->serial_cycles += cycles; + if ((gb->serial_cycles & gb->serial_length) != (previous_serial_cycles & gb->serial_length)) { + gb->serial_count++; + if (gb->serial_count == 8) { + gb->serial_length = 0; + gb->serial_count = 0; + gb->io_registers[GB_IO_SC] &= ~0x80; + gb->io_registers[GB_IO_IF] |= 8; + } + + gb->io_registers[GB_IO_SB] <<= 1; + + if (gb->serial_transfer_bit_end_callback) { + gb->io_registers[GB_IO_SB] |= gb->serial_transfer_bit_end_callback(gb); + } + else { + gb->io_registers[GB_IO_SB] |= 1; + } + + if (gb->serial_length) { + /* Still more bits to send */ + if (gb->serial_transfer_bit_start_callback) { + gb->serial_transfer_bit_start_callback(gb, gb->io_registers[GB_IO_SB] & 0x80); + } + } + + } + return; + +} + +void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) +{ + gb->apu.pcm_mask[0] = gb->apu.pcm_mask[1] = 0xFF; // Sort of hacky, but too many cross-component interactions to do it right + // Affected by speed boost + gb->dma_cycles += cycles; + + GB_timers_run(gb, cycles); + if (!gb->stopped) { + advance_serial(gb, cycles); // TODO: Verify what happens in STOP mode + } + + gb->debugger_ticks += cycles; + + if (!gb->cgb_double_speed) { + cycles <<= 1; + } + + // Not affected by speed boost + gb->double_speed_alignment += cycles; + gb->hdma_cycles += cycles; + gb->apu_output.sample_cycles += cycles; + gb->cycles_since_ir_change += cycles; + gb->cycles_since_input_ir_change += cycles; + gb->cycles_since_last_sync += cycles; + gb->cycles_since_run += cycles; + + if (gb->rumble_state) { + gb->rumble_on_cycles++; + } + else { + gb->rumble_off_cycles++; + } + + if (!gb->stopped) { // TODO: Verify what happens in STOP mode + GB_dma_run(gb); + GB_hdma_run(gb); + } + GB_apu_run(gb); + GB_display_run(gb, cycles); + GB_ir_run(gb); +} + +/* + This glitch is based on the expected results of mooneye-gb rapid_toggle test. + This glitch happens because how TIMA is increased, see GB_set_internal_div_counter. + According to GiiBiiAdvance, GBC's behavior is different, but this was not tested or implemented. +*/ +void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac) +{ + /* Glitch only happens when old_tac is enabled. */ + if (!(old_tac & 4)) return; + + unsigned old_clocks = GB_TAC_TRIGGER_BITS[old_tac & 3]; + unsigned new_clocks = GB_TAC_TRIGGER_BITS[new_tac & 3]; + + /* The bit used for overflow testing must have been 1 */ + if (gb->div_counter & old_clocks) { + /* And now either the timer must be disabled, or the new bit used for overflow testing be 0. */ + if (!(new_tac & 4) || gb->div_counter & new_clocks) { + increase_tima(gb); + } + } +} + +void GB_rtc_run(GB_gameboy_t *gb) +{ + if (gb->cartridge_type->mbc_type == GB_HUC3) { + time_t current_time = time(NULL); + while (gb->last_rtc_second / 60 < current_time / 60) { + gb->last_rtc_second += 60; + gb->huc3_minutes++; + if (gb->huc3_minutes == 60 * 24) { + gb->huc3_days++; + gb->huc3_minutes = 0; + } + } + return; + } + + if ((gb->rtc_real.high & 0x40) == 0) { /* is timer running? */ + time_t current_time = time(NULL); + + while (gb->last_rtc_second + 60 * 60 * 24 < current_time) { + gb->last_rtc_second += 60 * 60 * 24; + if (++gb->rtc_real.days == 0) { + if (gb->rtc_real.high & 1) { /* Bit 8 of days*/ + gb->rtc_real.high |= 0x80; /* Overflow bit */ + } + gb->rtc_real.high ^= 1; + } + } + + while (gb->last_rtc_second < current_time) { + gb->last_rtc_second++; + if (++gb->rtc_real.seconds == 60) { + gb->rtc_real.seconds = 0; + if (++gb->rtc_real.minutes == 60) { + gb->rtc_real.minutes = 0; + if (++gb->rtc_real.hours == 24) { + gb->rtc_real.hours = 0; + if (++gb->rtc_real.days == 0) { + if (gb->rtc_real.high & 1) { /* Bit 8 of days*/ + gb->rtc_real.high |= 0x80; /* Overflow bit */ + } + gb->rtc_real.high ^= 1; + } + } + } + } + } + } +} diff --git a/bsnes/gb/Core/timing.h b/bsnes/gb/Core/timing.h new file mode 100644 index 00000000..d4fa07f9 --- /dev/null +++ b/bsnes/gb/Core/timing.h @@ -0,0 +1,41 @@ +#ifndef timing_h +#define timing_h +#include "gb_struct_def.h" + +#ifdef GB_INTERNAL +void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles); +void GB_rtc_run(GB_gameboy_t *gb); +void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac); +bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */ +void GB_timing_sync(GB_gameboy_t *gb); + +enum { + GB_TIMA_RUNNING = 0, + GB_TIMA_RELOADING = 1, + GB_TIMA_RELOADED = 2 +}; + + +#define GB_SLEEP(gb, unit, state, cycles) do {\ + (gb)->unit##_cycles -= (cycles) * __state_machine_divisor; \ + if ((gb)->unit##_cycles <= 0) {\ + (gb)->unit##_state = state;\ + return;\ + unit##state:; \ + }\ +} while (0) + +#define GB_STATE_MACHINE(gb, unit, cycles, divisor) \ +static const int __state_machine_divisor = divisor;\ +(gb)->unit##_cycles += cycles; \ +if ((gb)->unit##_cycles <= 0) {\ + return;\ +}\ +switch ((gb)->unit##_state) +#endif + +#define GB_STATE(gb, unit, state) case state: goto unit##state + +#define GB_UNIT(unit) int32_t unit##_cycles, unit##_state + +#endif /* timing_h */ diff --git a/bsnes/gb/Core/workboy.c b/bsnes/gb/Core/workboy.c new file mode 100644 index 00000000..3b103796 --- /dev/null +++ b/bsnes/gb/Core/workboy.c @@ -0,0 +1,169 @@ +#include "gb.h" +#include + +static inline uint8_t int_to_bcd(uint8_t i) +{ + return (i % 10) + ((i / 10) << 4); +} + +static inline uint8_t bcd_to_int(uint8_t i) +{ + return (i & 0xF) + (i >> 4) * 10; +} + +/* + Note: This peripheral was never released. This is a hacky software reimplementation of it that allows + reaccessing all of the features present in Workboy's ROM. Some of the implementation details are + obviously wrong, but without access to the actual hardware, this is the best I can do. +*/ + +static void serial_start(GB_gameboy_t *gb, bool bit_received) +{ + gb->workboy.byte_being_received <<= 1; + gb->workboy.byte_being_received |= bit_received; + gb->workboy.bits_received++; + if (gb->workboy.bits_received == 8) { + gb->workboy.byte_to_send = 0; + if (gb->workboy.mode != 'W' && gb->workboy.byte_being_received == 'R') { + gb->workboy.byte_to_send = 'D'; + gb->workboy.key = GB_WORKBOY_NONE; + gb->workboy.mode = gb->workboy.byte_being_received; + gb->workboy.buffer_index = 1; + + time_t time = gb->workboy_get_time_callback(gb); + struct tm tm; + tm = *localtime(&time); + memset(gb->workboy.buffer, 0, sizeof(gb->workboy.buffer)); + + gb->workboy.buffer[0] = 4; // Unknown, unused, but appears to be expected to be 4 + gb->workboy.buffer[2] = int_to_bcd(tm.tm_sec); // Seconds, BCD + gb->workboy.buffer[3] = int_to_bcd(tm.tm_min); // Minutes, BCD + gb->workboy.buffer[4] = int_to_bcd(tm.tm_hour); // Hours, BCD + gb->workboy.buffer[5] = int_to_bcd(tm.tm_mday); // Days, BCD. Upper most 2 bits are added to Year for some reason + gb->workboy.buffer[6] = int_to_bcd(tm.tm_mon + 1); // Months, BCD + gb->workboy.buffer[0xF] = tm.tm_year; // Years, plain number, since 1900 + + } + else if (gb->workboy.mode != 'W' && gb->workboy.byte_being_received == 'W') { + gb->workboy.byte_to_send = 'D'; // It is actually unknown what this value should be + gb->workboy.key = GB_WORKBOY_NONE; + gb->workboy.mode = gb->workboy.byte_being_received; + gb->workboy.buffer_index = 0; + } + else if (gb->workboy.mode != 'W' && (gb->workboy.byte_being_received == 'O' || gb->workboy.mode == 'O')) { + gb->workboy.mode = 'O'; + gb->workboy.byte_to_send = gb->workboy.key; + if (gb->workboy.key != GB_WORKBOY_NONE) { + if (gb->workboy.key & GB_WORKBOY_REQUIRE_SHIFT) { + gb->workboy.key &= ~GB_WORKBOY_REQUIRE_SHIFT; + if (gb->workboy.shift_down) { + gb->workboy.byte_to_send = gb->workboy.key; + gb->workboy.key = GB_WORKBOY_NONE; + } + else { + gb->workboy.byte_to_send = GB_WORKBOY_SHIFT_DOWN; + gb->workboy.shift_down = true; + } + } + else if (gb->workboy.key & GB_WORKBOY_FORBID_SHIFT) { + gb->workboy.key &= ~GB_WORKBOY_FORBID_SHIFT; + if (!gb->workboy.shift_down) { + gb->workboy.byte_to_send = gb->workboy.key; + gb->workboy.key = GB_WORKBOY_NONE; + } + else { + gb->workboy.byte_to_send = GB_WORKBOY_SHIFT_UP; + gb->workboy.shift_down = false; + } + } + else { + if (gb->workboy.key == GB_WORKBOY_SHIFT_DOWN) { + gb->workboy.shift_down = true; + gb->workboy.user_shift_down = true; + } + else if (gb->workboy.key == GB_WORKBOY_SHIFT_UP) { + gb->workboy.shift_down = false; + gb->workboy.user_shift_down = false; + } + gb->workboy.byte_to_send = gb->workboy.key; + gb->workboy.key = GB_WORKBOY_NONE; + } + } + } + else if (gb->workboy.mode == 'R') { + if (gb->workboy.buffer_index / 2 >= sizeof(gb->workboy.buffer)) { + gb->workboy.byte_to_send = 0; + } + else { + if (gb->workboy.buffer_index & 1) { + gb->workboy.byte_to_send = "0123456789ABCDEF"[gb->workboy.buffer[gb->workboy.buffer_index / 2] & 0xF]; + } + else { + gb->workboy.byte_to_send = "0123456789ABCDEF"[gb->workboy.buffer[gb->workboy.buffer_index / 2] >> 4]; + } + gb->workboy.buffer_index++; + } + } + else if (gb->workboy.mode == 'W') { + gb->workboy.byte_to_send = 'D'; + if (gb->workboy.buffer_index < 2) { + gb->workboy.buffer_index++; + } + else if ((gb->workboy.buffer_index - 2) < sizeof(gb->workboy.buffer)) { + gb->workboy.buffer[gb->workboy.buffer_index - 2] = gb->workboy.byte_being_received; + gb->workboy.buffer_index++; + if (gb->workboy.buffer_index - 2 == sizeof(gb->workboy.buffer)) { + struct tm tm = {0,}; + tm.tm_sec = bcd_to_int(gb->workboy.buffer[7]); + tm.tm_min = bcd_to_int(gb->workboy.buffer[8]); + tm.tm_hour = bcd_to_int(gb->workboy.buffer[9]); + tm.tm_mday = bcd_to_int(gb->workboy.buffer[0xA]); + tm.tm_mon = bcd_to_int(gb->workboy.buffer[0xB] & 0x3F) - 1; + tm.tm_year = (uint8_t)(gb->workboy.buffer[0x14] + (gb->workboy.buffer[0xA] >> 6)); // What were they thinking? + gb->workboy_set_time_callback(gb, mktime(&tm)); + gb->workboy.mode = 'O'; + } + } + } + gb->workboy.bits_received = 0; + gb->workboy.byte_being_received = 0; + } +} + +static bool serial_end(GB_gameboy_t *gb) +{ + bool ret = gb->workboy.bit_to_send; + gb->workboy.bit_to_send = gb->workboy.byte_to_send & 0x80; + gb->workboy.byte_to_send <<= 1; + return ret; +} + +void GB_connect_workboy(GB_gameboy_t *gb, + GB_workboy_set_time_callback set_time_callback, + GB_workboy_get_time_callback get_time_callback) +{ + memset(&gb->workboy, 0, sizeof(gb->workboy)); + GB_set_serial_transfer_bit_start_callback(gb, serial_start); + GB_set_serial_transfer_bit_end_callback(gb, serial_end); + gb->workboy_set_time_callback = set_time_callback; + gb->workboy_get_time_callback = get_time_callback; +} + +bool GB_workboy_is_enabled(GB_gameboy_t *gb) +{ + return gb->workboy.mode; +} + +void GB_workboy_set_key(GB_gameboy_t *gb, uint8_t key) +{ + if (gb->workboy.user_shift_down != gb->workboy.shift_down && + (key & (GB_WORKBOY_REQUIRE_SHIFT | GB_WORKBOY_FORBID_SHIFT)) == 0) { + if (gb->workboy.user_shift_down) { + key |= GB_WORKBOY_REQUIRE_SHIFT; + } + else { + key |= GB_WORKBOY_FORBID_SHIFT; + } + } + gb->workboy.key = key; +} diff --git a/bsnes/gb/Core/workboy.h b/bsnes/gb/Core/workboy.h new file mode 100644 index 00000000..d21f2731 --- /dev/null +++ b/bsnes/gb/Core/workboy.h @@ -0,0 +1,118 @@ +#ifndef workboy_h +#define workboy_h +#include +#include +#include +#include "gb_struct_def.h" + + +typedef struct { + uint8_t byte_to_send; + bool bit_to_send; + uint8_t byte_being_received; + uint8_t bits_received; + uint8_t mode; + uint8_t key; + bool shift_down; + bool user_shift_down; + uint8_t buffer[0x15]; + uint8_t buffer_index; // In nibbles during read, in bytes during write +} GB_workboy_t; + +typedef void (*GB_workboy_set_time_callback)(GB_gameboy_t *gb, time_t time); +typedef time_t (*GB_workboy_get_time_callback)(GB_gameboy_t *gb); + +enum { + GB_WORKBOY_NONE = 0xFF, + GB_WORKBOY_REQUIRE_SHIFT = 0x40, + GB_WORKBOY_FORBID_SHIFT = 0x80, + + GB_WORKBOY_CLOCK = 1, + GB_WORKBOY_TEMPERATURE = 2, + GB_WORKBOY_MONEY = 3, + GB_WORKBOY_CALCULATOR = 4, + GB_WORKBOY_DATE = 5, + GB_WORKBOY_CONVERSION = 6, + GB_WORKBOY_RECORD = 7, + GB_WORKBOY_WORLD = 8, + GB_WORKBOY_PHONE = 9, + GB_WORKBOY_ESCAPE = 10, + GB_WORKBOY_BACKSPACE = 11, + GB_WORKBOY_UNKNOWN = 12, + GB_WORKBOY_LEFT = 13, + GB_WORKBOY_Q = 17, + GB_WORKBOY_1 = 17 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_W = 18, + GB_WORKBOY_2 = 18 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_E = 19, + GB_WORKBOY_3 = 19 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_R = 20, + GB_WORKBOY_T = 21, + GB_WORKBOY_Y = 22 , + GB_WORKBOY_U = 23 , + GB_WORKBOY_I = 24, + GB_WORKBOY_EXCLAMATION_MARK = 24 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_O = 25, + GB_WORKBOY_TILDE = 25 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_P = 26, + GB_WORKBOY_ASTERISK = 26 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_DOLLAR = 27 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_HASH = 27 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_A = 28, + GB_WORKBOY_4 = 28 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_S = 29, + GB_WORKBOY_5 = 29 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_D = 30, + GB_WORKBOY_6 = 30 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_F = 31, + GB_WORKBOY_PLUS = 31 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_G = 32, + GB_WORKBOY_MINUS = 32 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_H = 33, + GB_WORKBOY_J = 34, + GB_WORKBOY_K = 35, + GB_WORKBOY_LEFT_PARENTHESIS = 35 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_L = 36, + GB_WORKBOY_RIGHT_PARENTHESIS = 36 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_SEMICOLON = 37 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_COLON = 37, + GB_WORKBOY_ENTER = 38, + GB_WORKBOY_SHIFT_DOWN = 39, + GB_WORKBOY_Z = 40, + GB_WORKBOY_7 = 40 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_X = 41, + GB_WORKBOY_8 = 41 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_C = 42, + GB_WORKBOY_9 = 42 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_V = 43, + GB_WORKBOY_DECIMAL_POINT = 43 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_B = 44, + GB_WORKBOY_PERCENT = 44 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_N = 45, + GB_WORKBOY_EQUAL = 45 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_M = 46, + GB_WORKBOY_COMMA = 47 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_LT = 47 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_DOT = 48 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_GT = 48 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_SLASH = 49 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_QUESTION_MARK = 49 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_SHIFT_UP = 50, + GB_WORKBOY_0 = 51 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_UMLAUT = 51, + GB_WORKBOY_SPACE = 52, + GB_WORKBOY_QUOTE = 53 | GB_WORKBOY_FORBID_SHIFT, + GB_WORKBOY_AT = 53 | GB_WORKBOY_REQUIRE_SHIFT, + GB_WORKBOY_UP = 54, + GB_WORKBOY_DOWN = 55, + GB_WORKBOY_RIGHT = 56, +}; + + +void GB_connect_workboy(GB_gameboy_t *gb, + GB_workboy_set_time_callback set_time_callback, + GB_workboy_get_time_callback get_time_callback); +bool GB_workboy_is_enabled(GB_gameboy_t *gb); +void GB_workboy_set_key(GB_gameboy_t *gb, uint8_t key); + +#endif diff --git a/bsnes/gb/HexFiend/HFAnnotatedTree.h b/bsnes/gb/HexFiend/HFAnnotatedTree.h new file mode 100644 index 00000000..32122a18 --- /dev/null +++ b/bsnes/gb/HexFiend/HFAnnotatedTree.h @@ -0,0 +1,57 @@ +// +// HFAnnotatedTree.h +// HexFiend_2 +// +// Copyright 2010 ridiculous_fish. All rights reserved. +// + +#import + +typedef unsigned long long (*HFAnnotatedTreeAnnotaterFunction_t)(id left, id right); + + +@interface HFAnnotatedTreeNode : NSObject { + HFAnnotatedTreeNode *left; + HFAnnotatedTreeNode *right; + HFAnnotatedTreeNode *parent; + uint32_t level; +@public + unsigned long long annotation; +} + +/* Pure virtual method, which must be overridden. */ +- (NSComparisonResult)compare:(HFAnnotatedTreeNode *)node; + +/* Returns the next in-order node. */ +- (id)nextNode; + +- (id)leftNode; +- (id)rightNode; +- (id)parentNode; + +#if ! NDEBUG +- (void)verifyIntegrity; +- (void)verifyAnnotation:(HFAnnotatedTreeAnnotaterFunction_t)annotater; +#endif + + +@end + + +@interface HFAnnotatedTree : NSObject { + HFAnnotatedTreeAnnotaterFunction_t annotater; + HFAnnotatedTreeNode *root; +} + +- (instancetype)initWithAnnotater:(HFAnnotatedTreeAnnotaterFunction_t)annotater; +- (void)insertNode:(HFAnnotatedTreeNode *)node; +- (void)removeNode:(HFAnnotatedTreeNode *)node; +- (id)rootNode; +- (id)firstNode; +- (BOOL)isEmpty; + +#if ! NDEBUG +- (void)verifyIntegrity; +#endif + +@end diff --git a/bsnes/gb/HexFiend/HFAnnotatedTree.m b/bsnes/gb/HexFiend/HFAnnotatedTree.m new file mode 100644 index 00000000..9e64b9ae --- /dev/null +++ b/bsnes/gb/HexFiend/HFAnnotatedTree.m @@ -0,0 +1,432 @@ +// +// HFAnnotatedTree.m +// HexFiend_2 +// +// Copyright 2010 ridiculous_fish. All rights reserved. +// + +#import "HFAnnotatedTree.h" + +#if NDEBUG +#define VERIFY_INTEGRITY() do { } while (0) +#else +#define VERIFY_INTEGRITY() [self verifyIntegrity] +#endif + +/* HFAnnotatedTree is an AA tree. */ + +static unsigned long long null_annotater(id left, id right) { USE(left); USE(right); return 0; } +static void skew(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree); +static BOOL split(HFAnnotatedTreeNode *oldparent, HFAnnotatedTree *tree); +static void rebalanceAfterLeafAdd(HFAnnotatedTreeNode *n, HFAnnotatedTree *tree); +static void delete(HFAnnotatedTreeNode *n, HFAnnotatedTree *tree); +static void verify_integrity(HFAnnotatedTreeNode *n); + +static HFAnnotatedTreeNode *next_node(HFAnnotatedTreeNode *node); + +static void insert(HFAnnotatedTreeNode *root, HFAnnotatedTreeNode *node, HFAnnotatedTree *tree); + +static inline HFAnnotatedTreeNode *get_parent(HFAnnotatedTreeNode *node); +static inline HFAnnotatedTreeNode *get_root(HFAnnotatedTree *tree); +static inline HFAnnotatedTreeNode *create_root(void); +static inline HFAnnotatedTreeAnnotaterFunction_t get_annotater(HFAnnotatedTree *tree); + +static void reannotate(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree); + +static HFAnnotatedTreeNode *first_node(HFAnnotatedTreeNode *node); + +static HFAnnotatedTreeNode *left_child(HFAnnotatedTreeNode *node); +static HFAnnotatedTreeNode *right_child(HFAnnotatedTreeNode *node); + +@implementation HFAnnotatedTree + +- (instancetype)initWithAnnotater:(HFAnnotatedTreeAnnotaterFunction_t)annot { + self = [super init]; + annotater = annot ? annot : null_annotater; + /* root is always an HFAnnotatedTreeNode with a left child but no right child */ + root = create_root(); + return self; +} + +- (void)dealloc { + [root release]; + [super dealloc]; +} + +- (id)rootNode { + return root; +} + +- (id)firstNode { + return first_node(root); +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + HFAnnotatedTree *copied = [[[self class] alloc] init]; + copied->annotater = annotater; + [copied->root release]; + copied->root = [root mutableCopyWithZone:zone]; + return copied; +} + +- (BOOL)isEmpty { + /* We're empty if our root has no children. */ + return left_child(root) == nil && right_child(root) == nil; +} + +- (void)insertNode:(HFAnnotatedTreeNode *)node { + HFASSERT(node != nil); + HFASSERT(get_parent(node) == nil); + /* Insert into the root */ + insert(root, [node retain], self); + VERIFY_INTEGRITY(); +} + +- (void)removeNode:(HFAnnotatedTreeNode *)node { + HFASSERT(node != nil); + HFASSERT(get_parent(node) != nil); + delete(node, self); + [node release]; + VERIFY_INTEGRITY(); +} + +#if ! NDEBUG +- (void)verifyIntegrity { + [root verifyIntegrity]; + [root verifyAnnotation:annotater]; +} +#endif + +static HFAnnotatedTreeNode *get_root(HFAnnotatedTree *tree) { + return tree->root; +} + +static HFAnnotatedTreeAnnotaterFunction_t get_annotater(HFAnnotatedTree *tree) { + return tree->annotater; +} + +@end + +@implementation HFAnnotatedTreeNode + +- (void)dealloc { + [left release]; + [right release]; + [super dealloc]; +} + +- (NSComparisonResult)compare:(HFAnnotatedTreeNode *)node { + USE(node); + UNIMPLEMENTED(); +} + +- (id)nextNode { + return next_node(self); +} + +- (id)leftNode { return left; } +- (id)rightNode { return right; } +- (id)parentNode { return parent; } + +- (id)mutableCopyWithZone:(NSZone *)zone { + HFAnnotatedTreeNode *copied = [[[self class] alloc] init]; + if (left) { + copied->left = [left mutableCopyWithZone:zone]; + copied->left->parent = copied; + } + if (right) { + copied->right = [right mutableCopyWithZone:zone]; + copied->right->parent = copied; + } + copied->level = level; + copied->annotation = annotation; + return copied; +} + +static HFAnnotatedTreeNode *left_child(HFAnnotatedTreeNode *node) { + return node->left; +} + +static HFAnnotatedTreeNode *right_child(HFAnnotatedTreeNode *node) { + return node->right; +} + + +static HFAnnotatedTreeNode *create_root(void) { + HFAnnotatedTreeNode *result = [[HFAnnotatedTreeNode alloc] init]; + result->level = UINT_MAX; //the root has a huge level + return result; +} + +static void reannotate(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree) { + HFASSERT(node != nil); + HFASSERT(tree != nil); + const HFAnnotatedTreeAnnotaterFunction_t annotater = get_annotater(tree); + node->annotation = annotater(node->left, node->right); +} + +static void insert(HFAnnotatedTreeNode *root, HFAnnotatedTreeNode *node, HFAnnotatedTree *tree) { + /* Insert node at the proper place in the tree. root is the root node, and we always insert to the left of root */ + BOOL left = YES; + HFAnnotatedTreeNode *parentNode = root, *currentChild; + /* Descend the tree until we find where to insert */ + while ((currentChild = (left ? parentNode->left : parentNode->right)) != nil) { + parentNode = currentChild; + left = ([parentNode compare:node] >= 0); //if parentNode is larger than the child, then the child goes to the left of node + } + + /* Now insert, potentially unbalancing the tree */ + if (left) { + parentNode->left = node; + } + else { + parentNode->right = node; + } + + /* Tell our node about its new parent */ + node->parent = parentNode; + + /* Rebalance and update annotations */ + rebalanceAfterLeafAdd(node, tree); +} + +static void skew(HFAnnotatedTreeNode *oldparent, HFAnnotatedTree *tree) { + HFAnnotatedTreeNode *newp = oldparent->left; + + if (oldparent->parent->left == oldparent) { + /* oldparent is the left child of its parent. Substitute in our left child. */ + oldparent->parent->left = newp; + } + else { + /* oldparent is the right child of its parent. Substitute in our left child. */ + oldparent->parent->right = newp; + } + + /* Tell the child about its new parent */ + newp->parent = oldparent->parent; + + /* Adopt its right child as our left child, and tell it about its new parent */ + oldparent->left = newp->right; + if (oldparent->left) oldparent->left->parent = oldparent; + + /* We are now the right child of the new parent */ + newp->right = oldparent; + oldparent->parent = newp; + + /* If we're now a leaf, our level is 1. Otherwise, it's one more than the level of our child. */ + oldparent->level = oldparent->left ? oldparent->left->level + 1 : 1; + + /* oldparent and newp both had their children changed, so need to be reannotated */ + reannotate(oldparent, tree); + reannotate(newp, tree); +} + +static BOOL split(HFAnnotatedTreeNode *oldparent, HFAnnotatedTree *tree) { + HFAnnotatedTreeNode *newp = oldparent->right; + if (newp && newp->right && newp->right->level == oldparent->level) { + if (oldparent->parent->left == oldparent) oldparent->parent->left = newp; + else oldparent->parent->right = newp; + newp->parent = oldparent->parent; + oldparent->parent = newp; + + oldparent->right = newp->left; + if (oldparent->right) oldparent->right->parent = oldparent; + newp->left = oldparent; + newp->level = oldparent->level + 1; + + /* oldparent and newp both had their children changed, so need to be reannotated */ + reannotate(oldparent, tree); + reannotate(newp, tree); + + return YES; + } + return NO; +} + +static void rebalanceAfterLeafAdd(HFAnnotatedTreeNode *node, HFAnnotatedTree *tree) { // n is a node that has just been inserted and is now a leaf node. + node->level = 1; + node->left = nil; + node->right = nil; + reannotate(node, tree); + HFAnnotatedTreeNode * const root = get_root(tree); + HFAnnotatedTreeNode *probe; + for (probe = node->parent; probe != root; probe = probe->parent) { + reannotate(probe, tree); + // At this point probe->parent->level == probe->level + if (probe->level != (probe->left ? probe->left->level + 1 : 1)) { + // At this point the tree is correct, except (AA2) for n->parent + skew(probe, tree); + // We handle it (a left add) by changing it into a right add using Skew + // If the original add was to the left side of a node that is on the + // right side of a horisontal link, probe now points to the rights side + // of the second horisontal link, which is correct. + + // However if the original add was to the left of node with a horizontal + // link, we must get to the right side of the second link. + if (!probe->right || probe->level != probe->right->level) probe = probe->parent; + } + if (! split(probe->parent, tree)) break; + } + while (probe) { + reannotate(probe, tree); + probe = probe->parent; + } +} + +static void delete(HFAnnotatedTreeNode *n, HFAnnotatedTree *tree) { // If n is not a leaf, we first swap it out with the leaf node that just + // precedes it. + HFAnnotatedTreeNode *leaf = n, *tmp; + + if (n->left) { + /* Descend the right subtree of our left child, to get the closest predecessor */ + for (leaf = n->left; leaf->right; leaf = leaf->right) {} + // When we stop, leaf has no 'right' child so it cannot have a left one + } + else if (n->right) { + /* We have no children that precede us, but we have a child after us, so use our closest successor */ + leaf = n->right; + } + + /* tmp is either the parent who loses the child, or tmp is our right subtree. Either way, we will have to reduce its level. */ + tmp = leaf->parent == n ? leaf : leaf->parent; + + /* Tell leaf's parent to forget about leaf */ + if (leaf->parent->left == leaf) { + leaf->parent->left = NULL; + } + else { + leaf->parent->right = NULL; + } + reannotate(leaf->parent, tree); + + if (n != leaf) { + /* Replace ourself as our parent's child with leaf */ + if (n->parent->left == n) n->parent->left = leaf; + else n->parent->right = leaf; + + /* Leaf's parent is our parent */ + leaf->parent = n->parent; + + /* Our left and right children are now leaf's left and right children */ + if (n->left) n->left->parent = leaf; + leaf->left = n->left; + if (n->right) n->right->parent = leaf; + leaf->right = n->right; + + /* Leaf's level is our level */ + leaf->level = n->level; + } + /* Since we adopted n's children, transferring the retain, tell n to forget about them so it doesn't release them */ + n->left = nil; + n->right = nil; + + // free (n); + + HFAnnotatedTreeNode * const root = get_root(tree); + while (tmp != root) { + reannotate(tmp, tree); + // One of tmp's childern had its level reduced + if (tmp->level > (tmp->left ? tmp->left->level + 1 : 1)) { // AA2 failed + tmp->level--; + if (split(tmp, tree)) { + if (split(tmp, tree)) skew(tmp->parent->parent, tree); + break; + } + tmp = tmp->parent; + } + else if (tmp->level <= (tmp->right ? tmp->right->level + 1 : 1)){ + break; + } + else { // AA3 failed + skew(tmp, tree); + //if (tmp->right) tmp->right->level = tmp->right->left ? tmp->right->left->level + 1 : 1; + if (tmp->level > tmp->parent->level) { + skew(tmp, tree); + split(tmp->parent->parent, tree); + break; + } + tmp = tmp->parent->parent; + } + } + while (tmp) { + reannotate(tmp, tree); + tmp = tmp->parent; + } +} + +static HFAnnotatedTreeNode *next_node(HFAnnotatedTreeNode *node) { + /* Return the next in-order node */ + HFAnnotatedTreeNode *result; + if (node->right) { + /* We have a right child, which is after us. Descend its left subtree. */ + result = node->right; + while (result->left) { + result = result->left; + } + } + else { + /* We have no right child. If we are our parent's left child, then our parent is after us. Otherwise, we're our parent's right child and it was before us, so ascend while we're the parent's right child. */ + result = node; + while (result->parent && result->parent->right == result) { + result = result->parent; + } + /* Now result is the left child of the parent (or has NULL parents), so its parent is the next node */ + result = result->parent; + } + /* Don't return the root */ + if (result != nil && result->parent == nil) { + result = next_node(result); + } + return result; +} + +static HFAnnotatedTreeNode *first_node(HFAnnotatedTreeNode *node) { + /* Return the first node */ + HFAnnotatedTreeNode *result = nil, *cursor = node->left; + while (cursor) { + /* Descend the left subtree */ + result = cursor; + cursor = cursor->left; + } + return result; +} + +static HFAnnotatedTreeNode *get_parent(HFAnnotatedTreeNode *node) { + HFASSERT(node != nil); + return node->parent; +} + +static void __attribute__((unused))verify_integrity(HFAnnotatedTreeNode *n) { + HFASSERT(!n->left || n->left->parent == n); + HFASSERT(!n->right || n->right->parent == n); + HFASSERT(!next_node(n) || [n compare:next_node(n)] <= 0); + HFASSERT(!n->parent || n->parent->level >= n->level); + if (n->parent == nil) { + /* root node */ + HFASSERT(n->level == UINT_MAX); + } + else { + /* non-root node */ + HFASSERT(n->level == (n->left == NULL ? 1 : n->left->level + 1)); + HFASSERT((n->level <= 1) || (n->right && n->level - n->right->level <= 1)); + } + HFASSERT(!n->parent || !n->parent->parent || + n->parent->parent->level > n->level); +} + +#if ! NDEBUG +- (void)verifyIntegrity { + [left verifyIntegrity]; + [right verifyIntegrity]; + verify_integrity(self); +} + +- (void)verifyAnnotation:(HFAnnotatedTreeAnnotaterFunction_t)annotater { + [left verifyAnnotation:annotater]; + [right verifyAnnotation:annotater]; + unsigned long long expectedAnnotation = annotater(left, right); + HFASSERT(annotation == expectedAnnotation); +} +#endif + +@end diff --git a/bsnes/gb/HexFiend/HFBTree.h b/bsnes/gb/HexFiend/HFBTree.h new file mode 100644 index 00000000..3bb8bd90 --- /dev/null +++ b/bsnes/gb/HexFiend/HFBTree.h @@ -0,0 +1,40 @@ +// +// HFBTree.h +// HexFiend +// +// + +#import + +typedef unsigned long long HFBTreeIndex; + +@class HFBTreeNode; + +@protocol HFBTreeEntry +- (unsigned long long)length; +@end + +@interface HFBTree : NSObject { + unsigned int depth; + HFBTreeNode *root; +} + +- (void)insertEntry:(id)entry atOffset:(HFBTreeIndex)offset; +- (id)entryContainingOffset:(HFBTreeIndex)offset beginningOffset:(HFBTreeIndex *)outBeginningOffset; +- (void)removeEntryAtOffset:(HFBTreeIndex)offset; +- (void)removeAllEntries; + +#if HFUNIT_TESTS +- (void)checkIntegrityOfCachedLengths; +- (void)checkIntegrityOfBTreeStructure; +#endif + +- (NSEnumerator *)entryEnumerator; +- (NSArray *)allEntries; + +- (HFBTreeIndex)length; + +/* Applies the given function to the entry at the given offset, continuing with subsequent entries until the function returns NO. Do not modify the tree from within this function. */ +- (void)applyFunction:(BOOL (*)(id entry, HFBTreeIndex offset, void *userInfo))func toEntriesStartingAtOffset:(HFBTreeIndex)offset withUserInfo:(void *)userInfo; + +@end diff --git a/bsnes/gb/HexFiend/HFBTree.m b/bsnes/gb/HexFiend/HFBTree.m new file mode 100644 index 00000000..5bb7806a --- /dev/null +++ b/bsnes/gb/HexFiend/HFBTree.m @@ -0,0 +1,1099 @@ +// +// HFBTree.m +// BTree +// +// Created by peter on 2/6/09. +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import "HFBTree.h" +#include + +#define FIXUP_LENGTHS 0 + +#define BTREE_BRANCH_ORDER 10 +#define BTREE_LEAF_ORDER 10 + +#define BTREE_ORDER 10 +#define BTREE_NODE_MINIMUM_VALUE_COUNT (BTREE_ORDER / 2) + +#define BTREE_LEAF_MINIMUM_VALUE_COUNT (BTREE_LEAF_ORDER / 2) + +#define BAD_INDEX ((ChildIndex_t)(-1)) +typedef unsigned int ChildIndex_t; + +/* How deep can our tree get? 128 is huge. */ +#define MAX_DEPTH 128 +#define BAD_DEPTH ((TreeDepth_t)(-1)) +typedef unsigned int TreeDepth_t; + +#define TreeEntry NSObject +#define HFBTreeLength(x) [(TreeEntry *)(x) length] + + +@class HFBTreeNode, HFBTreeBranch, HFBTreeLeaf; + +static TreeEntry *btree_search(HFBTree *tree, HFBTreeIndex offset, HFBTreeIndex *outBeginningOffset); +static id btree_insert_returning_retained_value_for_parent(HFBTree *tree, TreeEntry *entry, HFBTreeIndex offset); +static BOOL btree_remove(HFBTree *tree, HFBTreeIndex offset); +static void __attribute__((unused)) btree_recursive_check_integrity(HFBTree *tree, HFBTreeNode *branchOrLeaf, TreeDepth_t depth, HFBTreeNode **linkHelper); +#if FIXUP_LENGTHS +static HFBTreeIndex btree_recursive_fixup_cached_lengths(HFBTree *tree, HFBTreeNode *branchOrLeaf); +#endif +static HFBTreeIndex __attribute__((unused)) btree_recursive_check_integrity_of_cached_lengths(HFBTreeNode *branchOrLeaf); +static BOOL btree_are_cached_lengths_correct(HFBTreeNode *branchOrLeaf, HFBTreeIndex *outLength); +#if FIXUP_LENGTHS +static NSUInteger btree_entry_count(HFBTreeNode *branchOrLeaf); +#endif +static ChildIndex_t count_node_values(HFBTreeNode *node); +static HFBTreeIndex sum_child_lengths(const id *children, const BOOL isLeaf); +static HFBTreeNode *mutable_copy_node(HFBTreeNode *node, TreeDepth_t depth, HFBTreeNode **linkingHelper); + +#if NDEBUG +#define VERIFY_LENGTH(a) +#else +#define VERIFY_LENGTH(a) btree_recursive_check_integrity_of_cached_lengths((a)) +#endif + +#define IS_BRANCH(a) [(a) isKindOfClass:[HFBTreeBranch class]] +#define IS_LEAF(a) [(a) isKindOfClass:[HFBTreeLeaf class]] + +#define ASSERT_IS_BRANCH(a) HFASSERT(IS_BRANCH(a)) +#define ASSERT_IS_LEAF(a) HFASSERT(IS_LEAF(a)) + +#define GET_LENGTH(node, parentIsLeaf) ((parentIsLeaf) ? HFBTreeLength(node) : CHECK_CAST((node), HFBTreeNode)->subtreeLength) + +#define CHECK_CAST(a, b) ({HFASSERT([(a) isKindOfClass:[b class]]); (b *)(a);}) +#define CHECK_CAST_OR_NULL(a, b) ({HFASSERT((a == nil) || [(a) isKindOfClass:[b class]]); (b *)(a);}) + +#define DEFEAT_INLINE 1 + +#if DEFEAT_INLINE +#define FORCE_STATIC_INLINE static +#else +#define FORCE_STATIC_INLINE static __inline__ __attribute__((always_inline)) +#endif + +@interface HFBTreeEnumerator : NSEnumerator { + HFBTreeLeaf *currentLeaf; + ChildIndex_t childIndex; +} + +- (instancetype)initWithLeaf:(HFBTreeLeaf *)leaf; + +@end + +@interface HFBTreeNode : NSObject { + @public + NSUInteger rc; + HFBTreeIndex subtreeLength; + HFBTreeNode *left, *right; + id children[BTREE_ORDER]; +} + +@end + +@implementation HFBTreeNode + +- (id)retain { + HFAtomicIncrement(&rc, NO); + return self; +} + +- (oneway void)release { + NSUInteger result = HFAtomicDecrement(&rc, NO); + if (result == (NSUInteger)(-1)) { + [self dealloc]; + } +} + +- (NSUInteger)retainCount { + return 1 + rc; +} + +- (void)dealloc { + for (ChildIndex_t i=0; i < BTREE_BRANCH_ORDER; i++) { + if (! children[i]) break; + [children[i] release]; + } + [super dealloc]; +} + +- (NSString *)shortDescription { + return [NSString stringWithFormat:@"<%@: %p (%llu)>", [self class], self, subtreeLength]; +} + +@end + +@interface HFBTreeBranch : HFBTreeNode +@end + +@implementation HFBTreeBranch + +- (NSString *)description { + const char *lengthsMatchString = (subtreeLength == sum_child_lengths(children, NO) ? "" : " INCONSISTENT "); + NSMutableString *s = [NSMutableString stringWithFormat:@"<%@: %p (length: %llu%s) (children: %u) (", [self class], self, subtreeLength, lengthsMatchString, count_node_values(self)]; + NSUInteger i; + for (i=0; i < BTREE_ORDER; i++) { + if (children[i] == nil) break; + [s appendFormat:@"%s%@", (i == 0 ? "" : ", "), [children[i] shortDescription]]; + } + [s appendString:@")>"]; + return s; +} + +@end + +@interface HFBTreeLeaf : HFBTreeNode +@end + +@implementation HFBTreeLeaf + +- (NSString *)description { + NSMutableString *s = [NSMutableString stringWithFormat:@"<%@: %p (%u) (", [self class], self, count_node_values(self)]; + NSUInteger i; + for (i=0; i < BTREE_ORDER; i++) { + if (children[i] == nil) break; + [s appendFormat:@"%s%@", (i == 0 ? "" : ", "), children[i]]; + } + [s appendString:@")>"]; + return s; +} + +@end + +@implementation HFBTree + +- (instancetype)init { + self = [super init]; + depth = BAD_DEPTH; + root = nil; + return self; +} + +- (void)dealloc { + [root release]; + [super dealloc]; +} + +#if HFUNIT_TESTS +- (void)checkIntegrityOfCachedLengths { + if (root == nil) { + /* nothing */ + } + else { + btree_recursive_check_integrity_of_cached_lengths(root); + } +} + +- (void)checkIntegrityOfBTreeStructure { + if (depth == BAD_DEPTH) { + HFASSERT(root == nil); + } + else { + HFBTreeNode *linkHelper[MAX_DEPTH + 1] = {}; + btree_recursive_check_integrity(self, root, depth, linkHelper); + } +} +#endif + +- (HFBTreeIndex)length { + if (root == nil) return 0; + return ((HFBTreeNode *)root)->subtreeLength; +} + +- (void)insertEntry:(id)entryObj atOffset:(HFBTreeIndex)offset { + TreeEntry *entry = (TreeEntry *)entryObj; //avoid a conflicting types warning + HFASSERT(entry); + HFASSERT(offset <= [self length]); + if (! root) { + HFASSERT([self length] == 0); + HFASSERT(depth == BAD_DEPTH); + HFBTreeLeaf *leaf = [[HFBTreeLeaf alloc] init]; + leaf->children[0] = [entry retain]; + leaf->subtreeLength = HFBTreeLength(entry); + root = leaf; + depth = 0; + } + else { + HFBTreeNode *newParentValue = btree_insert_returning_retained_value_for_parent(self, entry, offset); + if (newParentValue) { + HFBTreeBranch *newRoot = [[HFBTreeBranch alloc] init]; + newRoot->children[0] = root; //transfer our retain + newRoot->children[1] = newParentValue; //transfer the retain we got from the function + newRoot->subtreeLength = HFSum(root->subtreeLength, newParentValue->subtreeLength); + root = newRoot; + depth++; + HFASSERT(depth <= MAX_DEPTH); + } +#if FIXUP_LENGTHS + HFBTreeIndex outLength = -1; + if (! btree_are_cached_lengths_correct(root, &outLength)) { + puts("Fixed up length after insertion"); + btree_recursive_fixup_cached_lengths(self, root); + } +#endif + } +} + +- (TreeEntry *)entryContainingOffset:(HFBTreeIndex)offset beginningOffset:(HFBTreeIndex *)outBeginningOffset { + HFASSERT(root != nil); + return btree_search(self, offset, outBeginningOffset); +} + +- (void)removeAllEntries { + [root release]; + root = nil; + depth = BAD_DEPTH; +} + +- (void)removeEntryAtOffset:(HFBTreeIndex)offset { + HFASSERT(root != nil); +#if FIXUP_LENGTHS + const NSUInteger beforeCount = btree_entry_count(root); +#endif + BOOL deleteRoot = btree_remove(self, offset); + if (deleteRoot) { + HFASSERT(count_node_values(root) <= 1); + id newRoot = [root->children[0] retain]; //may be nil! + [root release]; + root = newRoot; + depth--; + } +#if FIXUP_LENGTHS + const NSUInteger afterCount = btree_entry_count(root); + if (beforeCount != afterCount + 1) { + NSLog(@"Bad counts: before %lu, after %lu", beforeCount, afterCount); + } + HFBTreeIndex outLength = -1; + static NSUInteger fixupCount; + if (! btree_are_cached_lengths_correct(root, &outLength)) { + fixupCount++; + printf("Fixed up length after deletion (%lu)\n", (unsigned long)fixupCount); + btree_recursive_fixup_cached_lengths(self, root); + } + else { + //printf("Length post-deletion was OK! (%lu)\n", fixupCount); + } +#endif +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + USE(zone); + HFBTree *result = [[[self class] alloc] init]; + result->depth = depth; + HFBTreeNode *linkingHelper[MAX_DEPTH + 1]; + bzero(linkingHelper, (1 + depth) * sizeof *linkingHelper); + result->root = mutable_copy_node(root, depth, linkingHelper); + return result; +} + +FORCE_STATIC_INLINE ChildIndex_t count_node_values(HFBTreeNode *node) { + ChildIndex_t count; + for (count=0; count < BTREE_LEAF_ORDER; count++) { + if (node->children[count] == nil) break; + } + return count; +} + +FORCE_STATIC_INLINE HFBTreeIndex sum_child_lengths(const id *children, const BOOL isLeaf) { + HFBTreeIndex result = 0; + for (ChildIndex_t childIndex = 0; childIndex < BTREE_ORDER; childIndex++) { + id child = children[childIndex]; + if (! child) break; + HFBTreeIndex childLength = GET_LENGTH(child, isLeaf); + result = HFSum(result, childLength); + } + return result; +} + +FORCE_STATIC_INLINE HFBTreeIndex sum_N_child_lengths(const id *children, ChildIndex_t numChildren, const BOOL isLeaf) { + HFBTreeIndex result = 0; + for (ChildIndex_t childIndex = 0; childIndex < numChildren; childIndex++) { + id child = children[childIndex]; + HFASSERT(child != NULL); + HFBTreeIndex childLength = GET_LENGTH(child, isLeaf); + result = HFSum(result, childLength); + } + return result; +} + +FORCE_STATIC_INLINE ChildIndex_t index_containing_offset(HFBTreeNode *node, HFBTreeIndex offset, HFBTreeIndex * restrict outOffset, const BOOL isLeaf) { + ChildIndex_t childIndex; + HFBTreeIndex previousSum = 0; + const id *children = node->children; + for (childIndex = 0; childIndex < BTREE_ORDER; childIndex++) { + HFASSERT(children[childIndex] != nil); + HFBTreeIndex childLength = GET_LENGTH(children[childIndex], isLeaf); + HFBTreeIndex newSum = HFSum(childLength, previousSum); + if (newSum > offset) { + break; + } + previousSum = newSum; + } + *outOffset = previousSum; + return childIndex; +} + +FORCE_STATIC_INLINE id child_containing_offset(HFBTreeNode *node, HFBTreeIndex offset, HFBTreeIndex * restrict outOffset, const BOOL isLeaf) { + return node->children[index_containing_offset(node, offset, outOffset, isLeaf)]; +} + +FORCE_STATIC_INLINE ChildIndex_t index_for_child_at_offset(HFBTreeNode *node, HFBTreeIndex offset, const BOOL isLeaf) { + ChildIndex_t childIndex; + HFBTreeIndex previousSum = 0; + id *const children = node->children; + for (childIndex = 0; childIndex < BTREE_ORDER; childIndex++) { + if (previousSum == offset) break; + HFASSERT(children[childIndex] != nil); + HFBTreeIndex childLength = GET_LENGTH(children[childIndex], isLeaf); + previousSum = HFSum(childLength, previousSum); + HFASSERT(previousSum <= offset); + } + HFASSERT(childIndex <= BTREE_ORDER); //note we allow the child index to be one past the end (in which case we are sure to split the node) + HFASSERT(previousSum == offset); //but we still require the offset to be the sum of all the lengths of this node + return childIndex; +} + +FORCE_STATIC_INLINE ChildIndex_t child_index_for_insertion_at_offset(HFBTreeBranch *branch, HFBTreeIndex insertionOffset, HFBTreeIndex *outPriorCombinedOffset) { + ChildIndex_t indexForInsertion; + HFBTreeIndex priorCombinedOffset = 0; + id *const children = branch->children; + for (indexForInsertion = 0; indexForInsertion < BTREE_BRANCH_ORDER; indexForInsertion++) { + if (! children[indexForInsertion]) break; + HFBTreeNode *childNode = CHECK_CAST(children[indexForInsertion], HFBTreeNode); + HFBTreeIndex subtreeLength = childNode->subtreeLength; + HFASSERT(subtreeLength > 0); + HFBTreeIndex newOffset = HFSum(priorCombinedOffset, subtreeLength); + if (newOffset >= insertionOffset) { + break; + } + priorCombinedOffset = newOffset; + } + *outPriorCombinedOffset = priorCombinedOffset; + return indexForInsertion; +} + +FORCE_STATIC_INLINE ChildIndex_t child_index_for_deletion_at_offset(HFBTreeBranch *branch, HFBTreeIndex deletionOffset, HFBTreeIndex *outPriorCombinedOffset) { + ChildIndex_t indexForDeletion; + HFBTreeIndex priorCombinedOffset = 0; + for (indexForDeletion = 0; indexForDeletion < BTREE_BRANCH_ORDER; indexForDeletion++) { + HFASSERT(branch->children[indexForDeletion] != nil); + HFBTreeNode *childNode = CHECK_CAST(branch->children[indexForDeletion], HFBTreeNode); + HFBTreeIndex subtreeLength = childNode->subtreeLength; + HFASSERT(subtreeLength > 0); + HFBTreeIndex newOffset = HFSum(priorCombinedOffset, subtreeLength); + if (newOffset > deletionOffset) { + /* Key difference between insertion and deletion: insertion uses >=, while deletion uses > */ + break; + } + priorCombinedOffset = newOffset; + } + *outPriorCombinedOffset = priorCombinedOffset; + return indexForDeletion; +} + +FORCE_STATIC_INLINE void insert_value_into_array(id value, NSUInteger insertionIndex, id *array, NSUInteger arrayCount) { + HFASSERT(insertionIndex <= arrayCount); + HFASSERT(arrayCount > 0); + NSUInteger pushingIndex = arrayCount - 1; + while (pushingIndex > insertionIndex) { + array[pushingIndex] = array[pushingIndex - 1]; + pushingIndex--; + } + array[insertionIndex] = [value retain]; +} + + +FORCE_STATIC_INLINE void remove_value_from_array(NSUInteger removalIndex, id *array, NSUInteger arrayCount) { + HFASSERT(removalIndex < arrayCount); + HFASSERT(arrayCount > 0); + HFASSERT(array[removalIndex] != nil); + [array[removalIndex] release]; + for (NSUInteger pullingIndex = removalIndex + 1; pullingIndex < arrayCount; pullingIndex++) { + array[pullingIndex - 1] = array[pullingIndex]; + } + array[arrayCount - 1] = nil; +} + +static void split_array(const restrict id *values, ChildIndex_t valueCount, restrict id *left, restrict id *right, ChildIndex_t leftArraySizeForClearing) { + const ChildIndex_t midPoint = valueCount/2; + ChildIndex_t inputIndex = 0, outputIndex = 0; + while (inputIndex < midPoint) { + left[outputIndex++] = values[inputIndex++]; + } + + /* Clear the remainder of our left array. Right array does not have to be cleared. */ + HFASSERT(outputIndex <= leftArraySizeForClearing); + while (outputIndex < leftArraySizeForClearing) { + left[outputIndex++] = nil; + } + + /* Move the second half of our values into the right array */ + outputIndex = 0; + while (inputIndex < valueCount) { + right[outputIndex++] = values[inputIndex++]; + } +} + +FORCE_STATIC_INLINE HFBTreeNode *add_child_to_node_possibly_creating_split(HFBTreeNode *node, id value, ChildIndex_t insertionLocation, BOOL isLeaf) { + ChildIndex_t childCount = count_node_values(node); + HFASSERT(insertionLocation <= childCount); + if (childCount < BTREE_ORDER) { + /* No need to make a split */ + insert_value_into_array(value, insertionLocation, node->children, childCount + 1); + node->subtreeLength = HFSum(node->subtreeLength, GET_LENGTH(value, isLeaf)); + return nil; + } + + HFASSERT(node->children[BTREE_ORDER - 1] != nil); /* we require that it be full */ + id allEntries[BTREE_ORDER + 1]; + memcpy(allEntries, node->children, BTREE_ORDER * sizeof *node->children); + allEntries[BTREE_ORDER] = nil; + + /* insert_value_into_array applies a retain, so allEntries owns a retain on its values */ + insert_value_into_array(value, insertionLocation, allEntries, BTREE_ORDER + 1); + HFBTreeNode *newNode = [[[node class] alloc] init]; + + /* figure out our total length */ + HFBTreeIndex totalLength = HFSum(node->subtreeLength, GET_LENGTH(value, isLeaf)); + + /* Distribute half our values to the new leaf */ + split_array(allEntries, sizeof allEntries / sizeof *allEntries, node->children, newNode->children, BTREE_ORDER); + + /* figure out how much is in the new array */ + HFBTreeIndex newNodeLength = sum_child_lengths(newNode->children, isLeaf); + + /* update our lengths */ + HFASSERT(newNodeLength < totalLength); + newNode->subtreeLength = newNodeLength; + node->subtreeLength = totalLength - newNodeLength; + + /* Link it in */ + HFBTreeNode *rightNode = node->right; + newNode->right = rightNode; + if (rightNode) rightNode->left = newNode; + newNode->left = node; + node->right = newNode; + return newNode; +} + +FORCE_STATIC_INLINE void add_values_to_array(const id * restrict srcValues, NSUInteger amountToCopy, id * restrict targetValues, NSUInteger amountToPush) { + // a pushed value at index X goes to index X + amountToCopy + NSUInteger pushIndex = amountToPush; + while (pushIndex--) { + targetValues[amountToCopy + pushIndex] = targetValues[pushIndex]; + } + for (NSUInteger i = 0; i < amountToCopy; i++) { + targetValues[i] = [srcValues[i] retain]; + } +} + +FORCE_STATIC_INLINE void remove_values_from_array(id * restrict array, NSUInteger amountToRemove, NSUInteger totalArrayLength) { + HFASSERT(totalArrayLength >= amountToRemove); + /* Release existing values */ + NSUInteger i; + for (i=0; i < amountToRemove; i++) { + [array[i] release]; + } + /* Move remaining values */ + for (i=amountToRemove; i < totalArrayLength; i++) { + array[i - amountToRemove] = array[i]; + } + /* Clear the end */ + for (i=totalArrayLength - amountToRemove; i < totalArrayLength; i++) { + array[i] = nil; + } +} + +FORCE_STATIC_INLINE BOOL rebalance_node_by_distributing_to_neighbors(HFBTreeNode *node, ChildIndex_t childCount, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childCount < BTREE_NODE_MINIMUM_VALUE_COUNT); + BOOL result = NO; + HFBTreeNode *leftNeighbor = node->left, *rightNeighbor = node->right; + const ChildIndex_t leftSpaceAvailable = (leftNeighbor ? BTREE_ORDER - count_node_values(leftNeighbor) : 0); + const ChildIndex_t rightSpaceAvailable = (rightNeighbor ? BTREE_ORDER - count_node_values(rightNeighbor) : 0); + if (leftSpaceAvailable + rightSpaceAvailable >= childCount) { + /* We have enough space to redistribute. Try to do it in such a way that both neighbors end up with the same number of items. */ + ChildIndex_t itemCountForLeft = 0, itemCountForRight = 0, itemCountRemaining = childCount; + if (leftSpaceAvailable > rightSpaceAvailable) { + ChildIndex_t amountForLeft = MIN(leftSpaceAvailable - rightSpaceAvailable, itemCountRemaining); + itemCountForLeft += amountForLeft; + itemCountRemaining -= amountForLeft; + } + else if (rightSpaceAvailable > leftSpaceAvailable) { + ChildIndex_t amountForRight = MIN(rightSpaceAvailable - leftSpaceAvailable, itemCountRemaining); + itemCountForRight += amountForRight; + itemCountRemaining -= amountForRight; + } + /* Now distribute the remainder (if any) evenly, preferring the remainder to go left, because it is slightly cheaper to append to the left than prepend to the right */ + itemCountForRight += itemCountRemaining / 2; + itemCountForLeft += itemCountRemaining - (itemCountRemaining / 2); + HFASSERT(itemCountForLeft <= leftSpaceAvailable); + HFASSERT(itemCountForRight <= rightSpaceAvailable); + HFASSERT(itemCountForLeft + itemCountForRight == childCount); + + if (itemCountForLeft > 0) { + /* append to the end */ + HFBTreeIndex additionalLengthForLeft = sum_N_child_lengths(node->children, itemCountForLeft, isLeaf); + leftNeighbor->subtreeLength = HFSum(leftNeighbor->subtreeLength, additionalLengthForLeft); + add_values_to_array(node->children, itemCountForLeft, leftNeighbor->children + BTREE_ORDER - leftSpaceAvailable, 0); + HFASSERT(leftNeighbor->subtreeLength == sum_child_lengths(leftNeighbor->children, isLeaf)); + *modifiedLeftNeighbor = YES; + } + if (itemCountForRight > 0) { + /* append to the beginning */ + HFBTreeIndex additionalLengthForRight = sum_N_child_lengths(node->children + itemCountForLeft, itemCountForRight, isLeaf); + rightNeighbor->subtreeLength = HFSum(rightNeighbor->subtreeLength, additionalLengthForRight); + add_values_to_array(node->children + itemCountForLeft, itemCountForRight, rightNeighbor->children, BTREE_ORDER - rightSpaceAvailable); + HFASSERT(rightNeighbor->subtreeLength == sum_child_lengths(rightNeighbor->children, isLeaf)); + *modifiedRightNeighbor = YES; + } + /* Remove ourself from the linked list */ + if (leftNeighbor) { + leftNeighbor->right = rightNeighbor; + } + if (rightNeighbor) { + rightNeighbor->left = leftNeighbor; + } + /* Even though we've essentially orphaned ourself, we need to force ourselves consistent (by making ourselves empty) because our parent still references us, and we don't want to make our parent inconsistent. */ + for (ChildIndex_t childIndex = 0; node->children[childIndex] != nil; childIndex++) { + [node->children[childIndex] release]; + node->children[childIndex] = nil; + } + node->subtreeLength = 0; + + result = YES; + } + return result; +} + + +FORCE_STATIC_INLINE BOOL share_children(HFBTreeNode *node, ChildIndex_t childCount, HFBTreeNode *neighbor, BOOL isRightNeighbor, BOOL isLeaf) { + ChildIndex_t neighborCount = count_node_values(neighbor); + ChildIndex_t totalChildren = (childCount + neighborCount); + BOOL result = NO; + if (totalChildren <= 2 * BTREE_LEAF_ORDER && totalChildren >= 2 * BTREE_LEAF_MINIMUM_VALUE_COUNT) { + ChildIndex_t finalMyCount = totalChildren / 2; + ChildIndex_t finalNeighborCount = totalChildren - finalMyCount; + HFASSERT(finalNeighborCount < neighborCount); + HFASSERT(finalMyCount > childCount); + ChildIndex_t amountToTransfer = finalMyCount - childCount; + HFBTreeIndex lengthChange; + if (isRightNeighbor) { + /* Transfer from left end of right neighbor to this right end of this leaf. This retains the values. */ + add_values_to_array(neighbor->children, amountToTransfer, node->children + childCount, 0); + /* Remove from beginning of right neighbor. This releases them. */ + remove_values_from_array(neighbor->children, amountToTransfer, neighborCount); + lengthChange = sum_N_child_lengths(node->children + childCount, amountToTransfer, isLeaf); + } + else { + /* Transfer from right end of left neighbor to left end of this leaf */ + add_values_to_array(neighbor->children + neighborCount - amountToTransfer, amountToTransfer, node->children, childCount); + /* Remove from end of left neighbor */ + remove_values_from_array(neighbor->children + neighborCount - amountToTransfer, amountToTransfer, amountToTransfer); + lengthChange = sum_N_child_lengths(node->children, amountToTransfer, isLeaf); + } + HFASSERT(lengthChange <= neighbor->subtreeLength); + neighbor->subtreeLength -= lengthChange; + node->subtreeLength = HFSum(node->subtreeLength, lengthChange); + HFASSERT(count_node_values(node) == finalMyCount); + HFASSERT(count_node_values(neighbor) == finalNeighborCount); + result = YES; + } + return result; +} + +static BOOL rebalance_node_by_sharing_with_neighbors(HFBTreeNode *node, ChildIndex_t childCount, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childCount < BTREE_LEAF_MINIMUM_VALUE_COUNT); + BOOL result = NO; + HFBTreeNode *leftNeighbor = node->left, *rightNeighbor = node->right; + if (leftNeighbor) { + result = share_children(node, childCount, leftNeighbor, NO, isLeaf); + if (result) *modifiedLeftNeighbor = YES; + } + if (! result && rightNeighbor) { + result = share_children(node, childCount, rightNeighbor, YES, isLeaf); + if (result) *modifiedRightNeighbor = YES; + } + return result; +} + +/* Return YES if this leaf should be removed after rebalancing. Other nodes are never removed. */ +FORCE_STATIC_INLINE BOOL rebalance_node_after_deletion(HFBTreeNode *node, ChildIndex_t childCount, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childCount < BTREE_LEAF_MINIMUM_VALUE_COUNT); + /* We may only delete this leaf, and not adjacent leaves. Thus our rebalancing strategy is: + If the items to the left or right have sufficient space to hold us, then push our values left or right, and delete this node. + Otherwise, steal items from the left until we have the same number of items. */ + BOOL deleteNode = NO; + if (rebalance_node_by_distributing_to_neighbors(node, childCount, isLeaf, modifiedLeftNeighbor, modifiedRightNeighbor)) { + deleteNode = YES; + //puts("rebalance_node_by_distributing_to_neighbors"); + } + else if (rebalance_node_by_sharing_with_neighbors(node, childCount, isLeaf, modifiedLeftNeighbor, modifiedRightNeighbor)) { + deleteNode = NO; + //puts("rebalance_node_by_sharing_with_neighbors"); + } + else { + [NSException raise:NSInternalInconsistencyException format:@"Unable to rebalance after deleting node %@", node]; + } + return deleteNode; +} + + +FORCE_STATIC_INLINE BOOL remove_value_from_node_with_possible_rebalance(HFBTreeNode *node, ChildIndex_t childIndex, BOOL isRootNode, BOOL isLeaf, BOOL * restrict modifiedLeftNeighbor, BOOL *restrict modifiedRightNeighbor) { + HFASSERT(childIndex < BTREE_ORDER); + HFASSERT(node != nil); + HFASSERT(node->children[childIndex] != nil); + HFBTreeIndex entryLength = GET_LENGTH(node->children[childIndex], isLeaf); + HFASSERT(entryLength <= node->subtreeLength); + node->subtreeLength -= entryLength; + BOOL deleteInputNode = NO; + +#if ! NDEBUG + const id savedChild = node->children[childIndex]; + NSUInteger childMultiplicity = 0; + NSUInteger v; + for (v = 0; v < BTREE_ORDER; v++) { + if (node->children[v] == savedChild) childMultiplicity++; + if (node->children[v] == nil) break; + } + +#endif + + /* Figure out how many children we have; start at one more than childIndex since we know that childIndex is a valid index */ + ChildIndex_t childCount; + for (childCount = childIndex + 1; childCount < BTREE_ORDER; childCount++) { + if (! node->children[childCount]) break; + } + + /* Remove our value at childIndex; this sends it a release message */ + remove_value_from_array(childIndex, node->children, childCount); + HFASSERT(childCount > 0); + childCount--; + +#if ! NDEBUG + for (v = 0; v < childCount; v++) { + if (node->children[v] == savedChild) childMultiplicity--; + } + HFASSERT(childMultiplicity == 1); +#endif + + if (childCount < BTREE_LEAF_MINIMUM_VALUE_COUNT && ! isRootNode) { + /* We have too few items; try to rebalance (this will always be possible except from the root node) */ + deleteInputNode = rebalance_node_after_deletion(node, childCount, isLeaf, modifiedLeftNeighbor, modifiedRightNeighbor); + } + else { + //NSLog(@"Deletion from %@ with %u remaining, %s root node, so no need to rebalance\n", node, childCount, isRootNode ? "is" : "is not"); + } + + return deleteInputNode; +} + +FORCE_STATIC_INLINE void update_node_having_changed_size_of_child(HFBTreeNode *node, BOOL isLeaf) { + HFBTreeIndex newLength = sum_child_lengths(node->children, isLeaf); + /* This should only be called if the length actually changes - so assert as such */ + /* I no longer think the above line is true. It's possible that we can delete a node, and then after a rebalance, we can become the same size we were before. */ + //HFASSERT(node->subtreeLength != newLength); + node->subtreeLength = newLength; +} + +struct SubtreeInfo_t { + HFBTreeBranch *branch; + ChildIndex_t childIndex; //childIndex is the index of the child of branch, not branch's index in its parent +}; + +static HFBTreeLeaf *btree_descend(HFBTree *tree, struct SubtreeInfo_t *outDescentInfo, HFBTreeIndex *insertionOffset, BOOL isForDelete) { + TreeDepth_t maxDepth = tree->depth; + HFASSERT(maxDepth != BAD_DEPTH && maxDepth <= MAX_DEPTH); + id currentBranchOrLeaf = tree->root; + HFBTreeIndex offsetForSubtree = *insertionOffset; + for (TreeDepth_t currentDepth = 0; currentDepth < maxDepth; currentDepth++) { + ASSERT_IS_BRANCH(currentBranchOrLeaf); + HFBTreeBranch *currentBranch = currentBranchOrLeaf; + HFBTreeIndex priorCombinedOffset = (HFBTreeIndex)-1; + ChildIndex_t nextChildIndex = (isForDelete ? child_index_for_deletion_at_offset : child_index_for_insertion_at_offset)(currentBranch, offsetForSubtree, &priorCombinedOffset); + outDescentInfo[currentDepth].branch = currentBranch; + outDescentInfo[currentDepth].childIndex = nextChildIndex; + offsetForSubtree -= priorCombinedOffset; + currentBranchOrLeaf = currentBranch->children[nextChildIndex]; + if (isForDelete) { + HFBTreeNode *node = currentBranchOrLeaf; + HFASSERT(node->subtreeLength > offsetForSubtree); + } + } + ASSERT_IS_LEAF(currentBranchOrLeaf); + *insertionOffset = offsetForSubtree; + return currentBranchOrLeaf; +} + +struct LeafInfo_t { + HFBTreeLeaf *leaf; + ChildIndex_t entryIndex; + HFBTreeIndex offsetOfEntryInTree; +}; + +static struct LeafInfo_t btree_find_leaf(HFBTree *tree, HFBTreeIndex offset) { + TreeDepth_t depth = tree->depth; + HFBTreeNode *currentNode = tree->root; + HFBTreeIndex remainingOffset = offset; + while (depth--) { + HFBTreeIndex beginningOffsetOfNode; + currentNode = child_containing_offset(currentNode, remainingOffset, &beginningOffsetOfNode, NO); + HFASSERT(beginningOffsetOfNode <= remainingOffset); + remainingOffset = remainingOffset - beginningOffsetOfNode; + } + ASSERT_IS_LEAF(currentNode); + HFBTreeIndex startOffsetOfEntry; + ChildIndex_t entryIndex = index_containing_offset(currentNode, remainingOffset, &startOffsetOfEntry, YES); + /* The offset of this entry is the requested offset minus the difference between its starting offset within the leaf and the requested offset within the leaf */ + HFASSERT(remainingOffset >= startOffsetOfEntry); + HFBTreeIndex offsetIntoEntry = remainingOffset - startOffsetOfEntry; + HFASSERT(offset >= offsetIntoEntry); + HFBTreeIndex beginningOffset = offset - offsetIntoEntry; + return (struct LeafInfo_t){.leaf = CHECK_CAST(currentNode, HFBTreeLeaf), .entryIndex = entryIndex, .offsetOfEntryInTree = beginningOffset}; +} + +static TreeEntry *btree_search(HFBTree *tree, HFBTreeIndex offset, HFBTreeIndex *outBeginningOffset) { + struct LeafInfo_t leafInfo = btree_find_leaf(tree, offset); + *outBeginningOffset = leafInfo.offsetOfEntryInTree; + return leafInfo.leaf->children[leafInfo.entryIndex]; +} + +static id btree_insert_returning_retained_value_for_parent(HFBTree *tree, TreeEntry *entry, HFBTreeIndex insertionOffset) { + struct SubtreeInfo_t descentInfo[MAX_DEPTH]; +#if ! NDEBUG + memset(descentInfo, -1, sizeof descentInfo); +#endif + HFBTreeIndex subtreeOffset = insertionOffset; + HFBTreeLeaf *leaf = btree_descend(tree, descentInfo, &subtreeOffset, NO); + ASSERT_IS_LEAF(leaf); + + ChildIndex_t insertionLocation = index_for_child_at_offset(leaf, subtreeOffset, YES); + HFBTreeNode *retainedValueToInsertIntoParentBranch = add_child_to_node_possibly_creating_split(leaf, entry, insertionLocation, YES); + + /* Walk up */ + TreeDepth_t depth = tree->depth; + HFASSERT(depth != BAD_DEPTH); + HFBTreeIndex entryLength = HFBTreeLength(entry); + while (depth--) { + HFBTreeBranch *branch = descentInfo[depth].branch; + branch->subtreeLength = HFSum(branch->subtreeLength, entryLength); + ChildIndex_t childIndex = descentInfo[depth].childIndex; + if (retainedValueToInsertIntoParentBranch) { + HFASSERT(branch->subtreeLength > retainedValueToInsertIntoParentBranch->subtreeLength); + /* Since we copied some stuff out from under ourselves, subtract its length */ + branch->subtreeLength -= retainedValueToInsertIntoParentBranch->subtreeLength; + HFBTreeNode *newRetainedValueToInsertIntoParentBranch = add_child_to_node_possibly_creating_split(branch, retainedValueToInsertIntoParentBranch, childIndex + 1, NO); + [retainedValueToInsertIntoParentBranch release]; + retainedValueToInsertIntoParentBranch = newRetainedValueToInsertIntoParentBranch; + } + } + return retainedValueToInsertIntoParentBranch; +} + +static BOOL btree_remove(HFBTree *tree, HFBTreeIndex deletionOffset) { + struct SubtreeInfo_t descentInfo[MAX_DEPTH]; +#if ! NDEBUG + memset(descentInfo, -1, sizeof descentInfo); +#endif + HFBTreeIndex subtreeOffset = deletionOffset; + HFBTreeLeaf *leaf = btree_descend(tree, descentInfo, &subtreeOffset, YES); + ASSERT_IS_LEAF(leaf); + + HFBTreeIndex previousOffsetSum = 0; + ChildIndex_t childIndex; + for (childIndex = 0; childIndex < BTREE_LEAF_ORDER; childIndex++) { + if (previousOffsetSum == subtreeOffset) break; + TreeEntry *entry = leaf->children[childIndex]; + HFASSERT(entry != nil); //if it were nil, then the offset is too large + HFBTreeIndex childLength = HFBTreeLength(entry); + previousOffsetSum = HFSum(childLength, previousOffsetSum); + } + HFASSERT(childIndex < BTREE_LEAF_ORDER); + HFASSERT(previousOffsetSum == subtreeOffset); + + TreeDepth_t depth = tree->depth; + HFASSERT(depth != BAD_DEPTH); + BOOL modifiedLeft = NO, modifiedRight = NO; + BOOL deleteNode = remove_value_from_node_with_possible_rebalance(leaf, childIndex, depth==0/*isRootNode*/, YES, &modifiedLeft, &modifiedRight); + HFASSERT(btree_are_cached_lengths_correct(leaf, NULL)); + while (depth--) { + HFBTreeBranch *branch = descentInfo[depth].branch; + ChildIndex_t branchChildIndex = descentInfo[depth].childIndex; + BOOL leftNeighborNeedsUpdating = modifiedLeft && branchChildIndex == 0; //if our child tweaked its left neighbor, and its left neighbor is not also a child of us, we need to inform its parent (which is our left neighbor) + BOOL rightNeighborNeedsUpdating = modifiedRight && (branchChildIndex + 1 == BTREE_BRANCH_ORDER || branch->children[branchChildIndex + 1] == NULL); //same goes for right + if (leftNeighborNeedsUpdating) { + HFASSERT(branch->left != NULL); +// NSLog(@"Updating lefty %p", branch->left); + update_node_having_changed_size_of_child(branch->left, NO); + } +#if ! NDEBUG + if (branch->left) HFASSERT(btree_are_cached_lengths_correct(branch->left, NULL)); +#endif + if (rightNeighborNeedsUpdating) { + HFASSERT(branch->right != NULL); +// NSLog(@"Updating righty %p", branch->right); + update_node_having_changed_size_of_child(branch->right, NO); + } +#if ! NDEBUG + if (branch->right) HFASSERT(btree_are_cached_lengths_correct(branch->right, NULL)); +#endif + update_node_having_changed_size_of_child(branch, NO); + modifiedLeft = NO; + modifiedRight = NO; + if (deleteNode) { + deleteNode = remove_value_from_node_with_possible_rebalance(branch, branchChildIndex, depth==0/*isRootNode*/, NO, &modifiedLeft, &modifiedRight); + } + else { + // update_node_having_changed_size_of_child(branch, NO); + // no need to delete parent nodes, so leave deleteNode as NO + } + /* Our parent may have to modify its left or right neighbor if we had to modify our left or right neighbor or if one of our children modified a neighbor that is not also a child of us. */ + modifiedLeft = modifiedLeft || leftNeighborNeedsUpdating; + modifiedRight = modifiedRight || rightNeighborNeedsUpdating; + } + + if (! deleteNode) { + /* Delete the root if it has one node and a depth of at least 1, or zero nodes and a depth of 0 */ + deleteNode = (tree->depth >= 1 && tree->root->children[1] == nil) || (tree->depth == 0 && tree->root->children[0] == nil); + } + return deleteNode; +} + +/* linkingHelper stores the last seen node for each depth. */ +static HFBTreeNode *mutable_copy_node(HFBTreeNode *node, TreeDepth_t depth, HFBTreeNode **linkingHelper) { + if (node == nil) return nil; + HFASSERT(depth != BAD_DEPTH); + Class class = (depth == 0 ? [HFBTreeLeaf class] : [HFBTreeBranch class]); + HFBTreeNode *result = [[class alloc] init]; + result->subtreeLength = node->subtreeLength; + + /* Link us in */ + HFBTreeNode *leftNeighbor = linkingHelper[0]; + if (leftNeighbor != nil) { + leftNeighbor->right = result; + result->left = leftNeighbor; + } + + /* Leave us for our future right neighbor to find */ + linkingHelper[0] = (void *)result; + + HFBTreeIndex index; + for (index = 0; index < BTREE_ORDER; index++) { + id child = node->children[index]; + if (! node->children[index]) break; + if (depth > 0) { + result->children[index] = mutable_copy_node(child, depth - 1, linkingHelper + 1); + } + else { + result->children[index] = [(TreeEntry *)child retain]; + } + } + return result; +} + +__attribute__((unused)) +static BOOL non_nulls_are_grouped_at_start(const id *ptr, NSUInteger count) { + BOOL hasSeenNull = NO; + for (NSUInteger i=0; i < count; i++) { + BOOL ptrIsNull = (ptr[i] == nil); + hasSeenNull = hasSeenNull || ptrIsNull; + if (hasSeenNull && ! ptrIsNull) { + return NO; + } + } + return YES; +} + + +static void btree_recursive_check_integrity(HFBTree *tree, HFBTreeNode *branchOrLeaf, TreeDepth_t depth, HFBTreeNode **linkHelper) { + HFASSERT(linkHelper[0] == branchOrLeaf->left); + if (linkHelper[0]) HFASSERT(linkHelper[0]->right == branchOrLeaf); + linkHelper[0] = branchOrLeaf; + + if (depth == 0) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + HFASSERT(non_nulls_are_grouped_at_start(leaf->children, BTREE_LEAF_ORDER)); + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + HFASSERT(non_nulls_are_grouped_at_start(branch->children, BTREE_BRANCH_ORDER)); + for (ChildIndex_t i = 0; i < BTREE_BRANCH_ORDER; i++) { + if (! branch->children[i]) break; + btree_recursive_check_integrity(tree, branch->children[i], depth - 1, linkHelper + 1); + } + } + ChildIndex_t childCount = count_node_values(branchOrLeaf); + if (depth < tree->depth) { // only the root may have fewer than BTREE_NODE_MINIMUM_VALUE_COUNT + HFASSERT(childCount >= BTREE_NODE_MINIMUM_VALUE_COUNT); + } + HFASSERT(childCount <= BTREE_ORDER); +} + +static HFBTreeIndex btree_recursive_check_integrity_of_cached_lengths(HFBTreeNode *branchOrLeaf) { + HFBTreeIndex result = 0; + if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i = 0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + result = HFSum(result, HFBTreeLength(leaf->children[i])); + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i = 0; i < BTREE_BRANCH_ORDER; i++) { + if (branch->children[i]) { + HFBTreeIndex subtreeLength = btree_recursive_check_integrity_of_cached_lengths(branch->children[i]); + result = HFSum(result, subtreeLength); + } + } + } + HFASSERT(result == branchOrLeaf->subtreeLength); + return result; +} + +static BOOL btree_are_cached_lengths_correct(HFBTreeNode *branchOrLeaf, HFBTreeIndex *outLength) { + if (! branchOrLeaf) { + if (outLength) *outLength = 0; + return YES; + } + HFBTreeIndex length = 0; + if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i=0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + length = HFSum(length, HFBTreeLength(leaf->children[i])); + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i=0; i < BTREE_BRANCH_ORDER; i++) { + if (! branch->children[i]) break; + HFBTreeIndex subLength = (HFBTreeIndex)-1; + if (! btree_are_cached_lengths_correct(branch->children[i], &subLength)) { + return NO; + } + length = HFSum(length, subLength); + } + } + if (outLength) *outLength = length; + return length == branchOrLeaf->subtreeLength; +} + +#if FIXUP_LENGTHS +static NSUInteger btree_entry_count(HFBTreeNode *branchOrLeaf) { + NSUInteger result = 0; + if (branchOrLeaf == nil) { + // do nothing + } + else if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i=0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + result++; + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i=0; i < BTREE_LEAF_ORDER; i++) { + if (! branch->children[i]) break; + result += btree_entry_count(branch->children[i]); + } + } + return result; +} + +static HFBTreeIndex btree_recursive_fixup_cached_lengths(HFBTree *tree, HFBTreeNode *branchOrLeaf) { + HFBTreeIndex result = 0; + if (IS_LEAF(branchOrLeaf)) { + HFBTreeLeaf *leaf = CHECK_CAST(branchOrLeaf, HFBTreeLeaf); + for (ChildIndex_t i = 0; i < BTREE_LEAF_ORDER; i++) { + if (! leaf->children[i]) break; + result = HFSum(result, HFBTreeLength(leaf->children[i])); + } + } + else { + HFBTreeBranch *branch = CHECK_CAST(branchOrLeaf, HFBTreeBranch); + for (ChildIndex_t i = 0; i < BTREE_BRANCH_ORDER; i++) { + if (! branch->children[i]) break; + btree_recursive_fixup_cached_lengths(tree, branch->children[i]); + result = HFSum(result, CHECK_CAST(branch->children[i], HFBTreeNode)->subtreeLength); + } + } + branchOrLeaf->subtreeLength = result; + return result; +} +#endif + +FORCE_STATIC_INLINE void btree_apply_function_to_entries(HFBTree *tree, HFBTreeIndex offset, BOOL (*func)(id, HFBTreeIndex, void *), void *userInfo) { + struct LeafInfo_t leafInfo = btree_find_leaf(tree, offset); + HFBTreeLeaf *leaf = leafInfo.leaf; + ChildIndex_t entryIndex = leafInfo.entryIndex; + HFBTreeIndex leafOffset = leafInfo.offsetOfEntryInTree; + BOOL continueApplying = YES; + while (leaf != NULL) { + for (; entryIndex < BTREE_LEAF_ORDER; entryIndex++) { + TreeEntry *entry = leaf->children[entryIndex]; + if (! entry) break; + continueApplying = func(entry, leafOffset, userInfo); + if (! continueApplying) break; + leafOffset = HFSum(leafOffset, HFBTreeLength(entry)); + } + if (! continueApplying) break; + leaf = CHECK_CAST_OR_NULL(leaf->right, HFBTreeLeaf); + entryIndex = 0; + } +} + +- (NSEnumerator *)entryEnumerator { + if (! root) return [@[] objectEnumerator]; + HFBTreeLeaf *leaf = btree_find_leaf(self, 0).leaf; + return [[[HFBTreeEnumerator alloc] initWithLeaf:leaf] autorelease]; +} + + +static BOOL add_to_array(id entry, HFBTreeIndex offset __attribute__((unused)), void *array) { + [(id)array addObject:entry]; + return YES; +} + +- (NSArray *)allEntries { + if (! root) return @[]; + NSUInteger treeCapacity = 1; + unsigned int depthIndex = depth; + while (depthIndex--) treeCapacity *= BTREE_ORDER; + NSMutableArray *result = [NSMutableArray arrayWithCapacity: treeCapacity/2]; //assume we're half full + btree_apply_function_to_entries(self, 0, add_to_array, result); + return result; +} + +- (void)applyFunction:(BOOL (*)(id entry, HFBTreeIndex offset, void *userInfo))func toEntriesStartingAtOffset:(HFBTreeIndex)offset withUserInfo:(void *)userInfo { + NSParameterAssert(func != NULL); + if (! root) return; + btree_apply_function_to_entries(self, offset, func, userInfo); +} + +@end + + +@implementation HFBTreeEnumerator + +- (instancetype)initWithLeaf:(HFBTreeLeaf *)leaf { + NSParameterAssert(leaf != nil); + ASSERT_IS_LEAF(leaf); + currentLeaf = leaf; + return self; +} + +- (id)nextObject { + if (! currentLeaf) return nil; + if (childIndex >= BTREE_LEAF_ORDER || currentLeaf->children[childIndex] == nil) { + childIndex = 0; + currentLeaf = CHECK_CAST_OR_NULL(currentLeaf->right, HFBTreeLeaf); + } + if (currentLeaf == nil) return nil; + HFASSERT(currentLeaf->children[childIndex] != nil); + return currentLeaf->children[childIndex++]; +} + +@end diff --git a/bsnes/gb/HexFiend/HFBTreeByteArray.h b/bsnes/gb/HexFiend/HFBTreeByteArray.h new file mode 100644 index 00000000..3c1aac07 --- /dev/null +++ b/bsnes/gb/HexFiend/HFBTreeByteArray.h @@ -0,0 +1,30 @@ +// +// HFBTreeByteArray.h +// HexFiend_2 +// +// Created by peter on 4/28/09. +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import + +@class HFBTree; + +/*! @class HFBTreeByteArray +@brief The principal efficient implementation of HFByteArray. + +HFBTreeByteArray is an efficient subclass of HFByteArray that stores @link HFByteSlice HFByteSlices@endlink, using a 10-way B+ tree. This allows for insertion, deletion, and searching in approximately log-base-10 time. + +Create an HFBTreeByteArray via \c -init. It has no methods other than those on HFByteArray. +*/ + +@interface HFBTreeByteArray : HFByteArray { +@private + HFBTree *btree; +} + +/*! Designated initializer for HFBTreeByteArray. +*/ +- (instancetype)init; + +@end diff --git a/bsnes/gb/HexFiend/HFBTreeByteArray.m b/bsnes/gb/HexFiend/HFBTreeByteArray.m new file mode 100644 index 00000000..33ba9673 --- /dev/null +++ b/bsnes/gb/HexFiend/HFBTreeByteArray.m @@ -0,0 +1,279 @@ +// +// HFBTreeByteArray.m +// HexFiend_2 +// +// Created by peter on 4/28/09. +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import + +@implementation HFBTreeByteArray + +- (instancetype)init { + if ((self = [super init])) { + btree = [[HFBTree alloc] init]; + } + return self; +} + +- (void)dealloc { + [btree release]; + [super dealloc]; +} + +- (unsigned long long)length { + return [btree length]; +} + +- (NSArray *)byteSlices { + return [btree allEntries]; +} + +- (NSEnumerator *)byteSliceEnumerator { + return [btree entryEnumerator]; +} + +- (NSString*)description { + NSMutableArray* result = [NSMutableArray array]; + NSEnumerator *enumer = [self byteSliceEnumerator]; + HFByteSlice *slice; + unsigned long long offset = 0; + while ((slice = [enumer nextObject])) { + unsigned long long length = [slice length]; + [result addObject:[NSString stringWithFormat:@"{%llu - %llu}", offset, length]]; + offset = HFSum(offset, length); + } + if (! [result count]) return @"(empty tree)"; + return [NSString stringWithFormat:@"<%@: %p>: %@", [self class], self, [result componentsJoinedByString:@" "]]; + +} + +struct HFBTreeByteArrayCopyInfo_t { + unsigned char *dst; + unsigned long long startingOffset; + NSUInteger remainingLength; +}; + +static BOOL copy_bytes(id entry, HFBTreeIndex offset, void *userInfo) { + struct HFBTreeByteArrayCopyInfo_t *info = userInfo; + HFByteSlice *slice = entry; + HFASSERT(slice != nil); + HFASSERT(info != NULL); + HFASSERT(offset <= info->startingOffset); + + unsigned long long sliceLength = [slice length]; + HFASSERT(sliceLength > 0); + unsigned long long offsetIntoSlice = info->startingOffset - offset; + HFASSERT(offsetIntoSlice < sliceLength); + NSUInteger amountToCopy = ll2l(MIN(info->remainingLength, sliceLength - offsetIntoSlice)); + HFRange srcRange = HFRangeMake(info->startingOffset - offset, amountToCopy); + [slice copyBytes:info->dst range:srcRange]; + info->dst += amountToCopy; + info->startingOffset = HFSum(info->startingOffset, amountToCopy); + info->remainingLength -= amountToCopy; + return info->remainingLength > 0; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); + HFASSERT(HFMaxRange(range) <= [self length]); + if (range.length > 0) { + struct HFBTreeByteArrayCopyInfo_t copyInfo = {.dst = dst, .remainingLength = ll2l(range.length), .startingOffset = range.location}; + [btree applyFunction:copy_bytes toEntriesStartingAtOffset:range.location withUserInfo:©Info]; + } +} + +- (HFByteSlice *)sliceContainingByteAtIndex:(unsigned long long)offset beginningOffset:(unsigned long long *)actualOffset { + return [btree entryContainingOffset:offset beginningOffset:actualOffset]; +} + +/* Given a HFByteArray and a range contained within it, return the first byte slice containing that range, and the range within that slice. Modifies the given range to reflect what you get when the returned slice is removed. */ +static inline HFByteSlice *findInitialSlice(HFBTree *btree, HFRange *inoutArrayRange, HFRange *outRangeWithinSlice) { + const HFRange arrayRange = *inoutArrayRange; + const unsigned long long arrayRangeEnd = HFMaxRange(arrayRange); + + unsigned long long offsetIntoSlice, lengthFromOffsetIntoSlice; + + unsigned long long beginningOffset; + HFByteSlice *slice = [btree entryContainingOffset:arrayRange.location beginningOffset:&beginningOffset]; + const unsigned long long sliceLength = [slice length]; + HFASSERT(beginningOffset <= arrayRange.location); + offsetIntoSlice = arrayRange.location - beginningOffset; + HFASSERT(offsetIntoSlice < sliceLength); + + unsigned long long sliceEndInArray = HFSum(sliceLength, beginningOffset); + if (sliceEndInArray <= arrayRangeEnd) { + /* Our slice ends before or at the requested range end */ + lengthFromOffsetIntoSlice = sliceLength - offsetIntoSlice; + } + else { + /* Our slice ends after the requested range end */ + unsigned long long overflow = sliceEndInArray - arrayRangeEnd; + HFASSERT(HFSum(overflow, offsetIntoSlice) < sliceLength); + lengthFromOffsetIntoSlice = sliceLength - HFSum(overflow, offsetIntoSlice); + } + + /* Set the out range to the input range minus the range consumed by the slice */ + inoutArrayRange->location = MIN(sliceEndInArray, arrayRangeEnd); + inoutArrayRange->length = arrayRangeEnd - inoutArrayRange->location; + + /* Set the out range within the slice to what we computed */ + *outRangeWithinSlice = HFRangeMake(offsetIntoSlice, lengthFromOffsetIntoSlice); + + return slice; +} + +- (BOOL)fastPathInsertByteSlice:(HFByteSlice *)slice atOffset:(unsigned long long)offset { + HFASSERT(offset > 0); + unsigned long long priorSliceOffset; + HFByteSlice *priorSlice = [btree entryContainingOffset:offset - 1 beginningOffset:&priorSliceOffset]; + HFByteSlice *appendedSlice = [priorSlice byteSliceByAppendingSlice:slice]; + if (appendedSlice) { + [btree removeEntryAtOffset:priorSliceOffset]; + [btree insertEntry:appendedSlice atOffset:priorSliceOffset]; + return YES; + } + else { + return NO; + } +} + +- (void)insertByteSlice:(HFByteSlice *)slice atOffset:(unsigned long long)offset { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + + if (offset == 0) { + [btree insertEntry:slice atOffset:0]; + } + else if (offset == [btree length]) { + if (! [self fastPathInsertByteSlice:slice atOffset:offset]) { + [btree insertEntry:slice atOffset:offset]; + } + } + else { + unsigned long long beginningOffset; + HFByteSlice *overlappingSlice = [btree entryContainingOffset:offset beginningOffset:&beginningOffset]; + if (beginningOffset == offset) { + if (! [self fastPathInsertByteSlice:slice atOffset:offset]) { + [btree insertEntry:slice atOffset:offset]; + } + } + else { + HFASSERT(offset > beginningOffset); + unsigned long long offsetIntoSlice = offset - beginningOffset; + unsigned long long sliceLength = [overlappingSlice length]; + HFASSERT(sliceLength > offsetIntoSlice); + HFByteSlice *left = [overlappingSlice subsliceWithRange:HFRangeMake(0, offsetIntoSlice)]; + HFByteSlice *right = [overlappingSlice subsliceWithRange:HFRangeMake(offsetIntoSlice, sliceLength - offsetIntoSlice)]; + [btree removeEntryAtOffset:beginningOffset]; + + [btree insertEntry:right atOffset:beginningOffset]; + + /* Try the fast appending path */ + HFByteSlice *joinedSlice = [left byteSliceByAppendingSlice:slice]; + if (joinedSlice) { + [btree insertEntry:joinedSlice atOffset:beginningOffset]; + } + else { + [btree insertEntry:slice atOffset:beginningOffset]; + [btree insertEntry:left atOffset:beginningOffset]; + } + } + } +} + +- (void)deleteBytesInRange:(HFRange)range { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + HFRange remainingRange = range; + + HFASSERT(HFMaxRange(range) <= [self length]); + if (range.length == 0) return; //nothing to delete + + //fast path for deleting everything + if (range.location == 0 && range.length == [self length]) { + [btree removeAllEntries]; + return; + } + + unsigned long long beforeLength = [self length]; + + unsigned long long rangeStartLocation = range.location; + HFByteSlice *beforeSlice = nil, *afterSlice = nil; + while (remainingRange.length > 0) { + HFRange rangeWithinSlice; + HFByteSlice *slice = findInitialSlice(btree, &remainingRange, &rangeWithinSlice); + const unsigned long long sliceLength = [slice length]; + const unsigned long long rangeWithinSliceEnd = HFMaxRange(rangeWithinSlice); + HFRange lefty = HFRangeMake(0, rangeWithinSlice.location); + HFRange righty = HFRangeMake(rangeWithinSliceEnd, sliceLength - rangeWithinSliceEnd); + HFASSERT(lefty.length == 0 || beforeSlice == nil); + HFASSERT(righty.length == 0 || afterSlice == nil); + + unsigned long long beginningOffset = remainingRange.location - HFMaxRange(rangeWithinSlice); + + if (lefty.length > 0){ + beforeSlice = [slice subsliceWithRange:lefty]; + rangeStartLocation = beginningOffset; + } + if (righty.length > 0) afterSlice = [slice subsliceWithRange:righty]; + + [btree removeEntryAtOffset:beginningOffset]; + remainingRange.location = beginningOffset; + } + if (afterSlice) { + [self insertByteSlice:afterSlice atOffset:rangeStartLocation]; + } + if (beforeSlice) { + [self insertByteSlice:beforeSlice atOffset:rangeStartLocation]; + } + + unsigned long long afterLength = [self length]; + HFASSERT(beforeLength - afterLength == range.length); +} + +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + + if (lrange.length > 0) { + [self deleteBytesInRange:lrange]; + } + if ([slice length] > 0) { + [self insertByteSlice:slice atOffset:lrange.location]; + } +} + +- (id)mutableCopyWithZone:(NSZone *)zone { + USE(zone); + HFBTreeByteArray *result = [[[self class] alloc] init]; + [result->btree release]; + result->btree = [btree mutableCopy]; + return result; +} + +- (id)subarrayWithRange:(HFRange)range { + if (range.location == 0 && range.length == [self length]) { + return [[self mutableCopy] autorelease]; + } + HFBTreeByteArray *result = [[[[self class] alloc] init] autorelease]; + HFRange remainingRange = range; + unsigned long long offsetInResult = 0; + while (remainingRange.length > 0) { + HFRange rangeWithinSlice; + HFByteSlice *slice = findInitialSlice(btree, &remainingRange, &rangeWithinSlice); + HFByteSlice *subslice; + if (rangeWithinSlice.location == 0 && rangeWithinSlice.length == [slice length]) { + subslice = slice; + } + else { + subslice = [slice subsliceWithRange:rangeWithinSlice]; + } + [result insertByteSlice:subslice atOffset:offsetInResult]; + offsetInResult = HFSum(offsetInResult, rangeWithinSlice.length); + } + return result; +} + +@end diff --git a/bsnes/gb/HexFiend/HFByteArray.h b/bsnes/gb/HexFiend/HFByteArray.h new file mode 100644 index 00000000..dd452d5b --- /dev/null +++ b/bsnes/gb/HexFiend/HFByteArray.h @@ -0,0 +1,180 @@ +// +// HFByteArray.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +@class HFByteSlice, HFFileReference, HFByteRangeAttributeArray; + +typedef NS_ENUM(NSUInteger, HFByteArrayDataStringType) { + HFHexDataStringType, + HFASCIIDataStringType +}; + + +/*! @class HFByteArray +@brief The principal Model class for HexFiend's MVC architecture. + +HFByteArray implements the Model portion of HexFiend.framework. It is logically a mutable, resizable array of bytes, with a 64 bit length. It is somewhat analagous to a 64 bit version of NSMutableData, except that it is designed to enable efficient (faster than O(n)) implementations of insertion and deletion. + +HFByteArray, being an abstract class, will raise an exception if you attempt to instantiate it directly. For most uses, instantiate HFBTreeByteArray instead, with the usual [[class alloc] init]. + +HFByteArray also exposes itself as an array of @link HFByteSlice HFByteSlices@endlink, which are logically immutable arrays of bytes. which is useful for operations such as file saving that need to access the underlying byte slices. + +HFByteArray contains a generation count, which is incremented whenever the HFByteArray changes (to allow caches to be implemented on top of it). It also includes the notion of locking: a locked HFByteArray will raise an exception if written to, but it may still be read. + +ByteArrays have the usual threading restrictions for non-concurrent data structures. It is safe to read an HFByteArray concurrently from multiple threads. It is not safe to read an HFByteArray while it is being modified from another thread, nor is it safe to modify one simultaneously from two threads. + +HFByteArray is an abstract class. It will raise an exception if you attempt to instantiate it directly. The principal concrete subclass is HFBTreeByteArray. +*/ + +@class HFByteRangeAttributeArray; + +@interface HFByteArray : NSObject { +@private + NSUInteger changeLockCounter; + NSUInteger changeGenerationCount; +} + +/*! @name Initialization + */ +//@{ +/*! Initialize to a byte array containing only the given slice. */ +- (instancetype)initWithByteSlice:(HFByteSlice *)slice; + +/*! Initialize to a byte array containing the slices of the given array. */ +- (instancetype)initWithByteArray:(HFByteArray *)array; +//@} + + +/*! @name Accessing raw data +*/ +//@{ + +/*! Returns the length of the HFByteArray as a 64 bit unsigned long long. This is an abstract method that concrete subclasses must override. */ +- (unsigned long long)length; + +/*! Copies a range of bytes into a buffer. This is an abstract method that concrete subclasses must override. */ +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range; +//@} + +/*! @name Accessing byte slices + Methods to access the byte slices underlying the HFByteArray. +*/ +//@{ +/*! Returns the contents of the receiver as an array of byte slices. This is an abstract method that concrete subclasses must override. */ +- (NSArray *)byteSlices; + +/*! Returns an NSEnumerator representing the byte slices of the receiver. This is implemented as enumerating over the result of -byteSlices, but subclasses can override this to be more efficient. */ +- (NSEnumerator *)byteSliceEnumerator; + +/*! Returns the byte slice containing the byte at the given index, and the actual offset of this slice. */ +- (HFByteSlice *)sliceContainingByteAtIndex:(unsigned long long)offset beginningOffset:(unsigned long long *)actualOffset; +//@} + +/*! @name Modifying the byte array + Methods to modify the given byte array. +*/ +//@{ +/*! Insert an HFByteSlice in the given range. The maximum value of the range must not exceed the length of the subarray. The length of the given slice is not required to be equal to length of the range - in other words, this method may change the length of the receiver. This is an abstract method that concrete subclasses must override. */ +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange; + +/*! Insert an HFByteArray in the given range. This is implemented via calling insertByteSlice:inRange: with the byte slices from the given byte array. */ +- (void)insertByteArray:(HFByteArray *)array inRange:(HFRange)lrange; + +/*! Delete bytes in the given range. This is implemented on the base class by creating an empty byte array and inserting it in the range to be deleted, via insertByteSlice:inRange:. */ +- (void)deleteBytesInRange:(HFRange)range; + +/*! Returns a new HFByteArray containing the given range. This is an abstract method that concrete subclasses must override. */ +- (HFByteArray *)subarrayWithRange:(HFRange)range; +//@} + +/*! @name Write locking and generation count + Methods to lock and query the lock that prevents writes. +*/ +//@{ + +/*! Increment the change lock. Until the change lock reaches 0, all modifications to the receiver will raise an exception. */ +- (void)incrementChangeLockCounter; + +/*! Decrement the change lock. If the change lock reaches 0, modifications will be allowed again. */ +- (void)decrementChangeLockCounter; + +/*! Query if the changes are locked. This method is KVO compliant. */ +- (BOOL)changesAreLocked; +//@} + +/* @name Generation count + Manipulate the generation count */ +// @{ +/*! Increments the generation count, unless the receiver is locked, in which case it raises an exception. All subclasses of HFByteArray should call this method at the beginning of any overridden method that may modify the receiver. + @param sel The selector that would modify the receiver (e.g. deleteBytesInRange:). This is usually _cmd. */ +- (void)incrementGenerationOrRaiseIfLockedForSelector:(SEL)sel; + +/*! Return the change generation count. Every change to the ByteArray increments this by one or more. This can be used for caching layers on top of HFByteArray, to known when to expire their cache. */ +- (NSUInteger)changeGenerationCount; + +//@} + + + +/*! @name Searching +*/ +//@{ +/*! Searches the receiver for a byte array matching findBytes within the given range, and returns the index that it was found. This is a concrete method on HFByteArray. + @param findBytes The HFByteArray containing the data to be found (the needle to the receiver's haystack). + @param range The range of the receiver in which to search. The end of the range must not exceed the receiver's length. + @param forwards If this is YES, then the first match within the range is returned. Otherwise the last is returned. + @param progressTracker An HFProgressTracker to allow progress reporting and cancelleation for the search operation. + @return The index in the receiver of bytes equal to findBytes, or ULLONG_MAX if the byte array was not found (or the operation was cancelled) +*/ +- (unsigned long long)indexOfBytesEqualToBytes:(HFByteArray *)findBytes inRange:(HFRange)range searchingForwards:(BOOL)forwards trackingProgress:(id)progressTracker; +//@} + +@end + + +/*! @category HFByteArray(HFFileWriting) + @brief HFByteArray methods for writing to files, and preparing other HFByteArrays for potentially destructive file writes. +*/ +@interface HFByteArray (HFFileWriting) +/*! Attempts to write the receiver to a file. This is a concrete method on HFByteArray. + @param targetURL A URL to the file to be written to. It is OK for the receiver to contain one or more instances of HFByteSlice that are sourced from the file. + @param progressTracker An HFProgressTracker to allow progress reporting and cancelleation for the write operation. + @param error An out NSError parameter. + @return YES if the write succeeded, NO if it failed. +*/ +- (BOOL)writeToFile:(NSURL *)targetURL trackingProgress:(id)progressTracker error:(NSError **)error; + +/*! Returns the ranges of the file that would be modified, if the receiver were written to it. This is useful (for example) in determining if the clipboard can be preserved after a save operation. This is a concrete method on HFByteArray. + @param reference An HFFileReference to the file to be modified + @return An array of @link HFRangeWrapper HFRangeWrappers@endlink, representing the ranges of the file that would be affected. If no range would be affected, the result is an empty array. +*/ +- (NSArray *)rangesOfFileModifiedIfSavedToFile:(HFFileReference *)reference; + +/*! Attempts to modify the receiver so that it no longer depends on any of the HFRanges in the array within the given file. It is not necessary to perform this operation on the byte array that is being written to the file. + @param ranges An array of HFRangeWrappers, representing ranges in the given file that the receiver should no longer depend on. + @param reference The HFFileReference that the receiver should no longer depend on. + @param hint A dictionary that can be used to improve the efficiency of the operation, by allowing multiple byte arrays to share the same state. If you plan to call this method on multiple byte arrays, pass the first one an empty NSMutableDictionary, and pass the same dictionary to subsequent calls. + @return A YES return indicates the operation was successful, and the receiver no longer contains byte slices that source data from any of the ranges of the given file (or never did). A NO return indicates that breaking the dependencies would require too much memory, and so the receiver still depends on some of those ranges. +*/ +- (BOOL)clearDependenciesOnRanges:(NSArray *)ranges inFile:(HFFileReference *)reference hint:(NSMutableDictionary *)hint; + +@end + + +/*! @category HFByteArray(HFAttributes) + @brief HFByteArray methods for attributes of byte arrays. +*/ +@interface HFByteArray (HFAttributes) + +/*! Returns a byte range attribute array for the bytes in the given range. */ +- (HFByteRangeAttributeArray *)attributesForBytesInRange:(HFRange)range; + +/*! Returns the HFByteArray level byte range attribute array. Default is to return nil. */ +- (HFByteRangeAttributeArray *)byteRangeAttributeArray; + +@end diff --git a/bsnes/gb/HexFiend/HFByteArray.m b/bsnes/gb/HexFiend/HFByteArray.m new file mode 100644 index 00000000..55f25cc8 --- /dev/null +++ b/bsnes/gb/HexFiend/HFByteArray.m @@ -0,0 +1,218 @@ +// +// HFByteArray.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + + +@implementation HFByteArray + +- (instancetype)init { + if ([self class] == [HFByteArray class]) { + [NSException raise:NSInvalidArgumentException format:@"init sent to HFByteArray, but HFByteArray is an abstract class. Instantiate one of its subclasses instead, like HFBTreeByteArray."]; + } + return [super init]; +} + +- (instancetype)initWithByteSlice:(HFByteSlice *)slice { + if(!(self = [self init])) return nil; + self = [self init]; + [self insertByteSlice:slice inRange:HFRangeMake(0, 0)]; + return self; +} + +- (instancetype)initWithByteArray:(HFByteArray *)array { + if(!(self = [self init])) return nil; + NSEnumerator *e = [array byteSliceEnumerator]; + HFByteSlice *slice; + while((slice = [e nextObject])) { + [self insertByteSlice:slice inRange:HFRangeMake([self length], 0)]; + } + return self; +} + +- (NSArray *)byteSlices { UNIMPLEMENTED(); } +- (unsigned long long)length { UNIMPLEMENTED(); } +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range { USE(dst); USE(range); UNIMPLEMENTED_VOID(); } +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange { USE(slice); USE(lrange); UNIMPLEMENTED_VOID(); } + +- (NSEnumerator *)byteSliceEnumerator { + return [[self byteSlices] objectEnumerator]; +} + +- (HFByteSlice *)sliceContainingByteAtIndex:(unsigned long long)offset beginningOffset:(unsigned long long *)actualOffset { + HFByteSlice *slice; + unsigned long long current = 0; + NSEnumerator *enumer = [self byteSliceEnumerator]; + while ((slice = [enumer nextObject])) { + unsigned long long sum = HFSum([slice length], current); + if (sum > offset) break; + current = sum; + } + if (actualOffset) *actualOffset = current; + return slice; +} + +- (void)insertByteArray:(HFByteArray*)array inRange:(HFRange)lrange { + REQUIRE_NOT_NULL(array); + HFASSERT(HFRangeIsSubrangeOfRange(lrange, HFRangeMake(0, [self length]))); +#ifndef NDEBUG + unsigned long long expectedLength = [self length] - lrange.length + [array length]; +#endif + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + NSEnumerator *sliceEnumerator; + HFByteSlice *byteSlice; + if (array == self) { + /* Guard against self insertion */ + sliceEnumerator = [[array byteSlices] objectEnumerator]; + } + else { + sliceEnumerator = [array byteSliceEnumerator]; + } + while ((byteSlice = [sliceEnumerator nextObject])) { + [self insertByteSlice:byteSlice inRange:lrange]; + lrange.location += [byteSlice length]; + lrange.length = 0; + } + /* If there were no slices, delete the lrange */ + if (lrange.length > 0) { + [self deleteBytesInRange:lrange]; + } +#ifndef NDEBUG + HFASSERT(expectedLength == [self length]); +#endif +} + +- (HFByteArray *)subarrayWithRange:(HFRange)range { USE(range); UNIMPLEMENTED(); } + +- (id)mutableCopyWithZone:(NSZone *)zone { + USE(zone); + return [[self subarrayWithRange:HFRangeMake(0, [self length])] retain]; +} + +- (id)copyWithZone:(NSZone *)zone { + USE(zone); + return [[self subarrayWithRange:HFRangeMake(0, [self length])] retain]; +} + +- (void)deleteBytesInRange:(HFRange)lrange { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + HFByteSlice* slice = [[HFFullMemoryByteSlice alloc] initWithData:[NSData data]]; + [self insertByteSlice:slice inRange:lrange]; + [slice release]; +} + +- (BOOL)isEqual:v { + REQUIRE_NOT_NULL(v); + if (self == v) return YES; + else if (! [v isKindOfClass:[HFByteArray class]]) return NO; + else { + HFByteArray* obj = v; + unsigned long long length = [self length]; + if (length != [obj length]) return NO; + unsigned long long offset; + unsigned char buffer1[1024]; + unsigned char buffer2[sizeof buffer1 / sizeof *buffer1]; + for (offset = 0; offset < length; offset += sizeof buffer1) { + size_t amountToGrab = sizeof buffer1; + if (amountToGrab > length - offset) amountToGrab = ll2l(length - offset); + [self copyBytes:buffer1 range:HFRangeMake(offset, amountToGrab)]; + [obj copyBytes:buffer2 range:HFRangeMake(offset, amountToGrab)]; + if (memcmp(buffer1, buffer2, amountToGrab)) return NO; + } + } + return YES; +} + +- (unsigned long long)indexOfBytesEqualToBytes:(HFByteArray *)findBytes inRange:(HFRange)range searchingForwards:(BOOL)forwards trackingProgress:(id)progressTracker { + UNIMPLEMENTED(); +} + +- (BOOL)_debugIsEqual:(HFByteArray *)v { + REQUIRE_NOT_NULL(v); + if (! [v isKindOfClass:[HFByteArray class]]) return NO; + HFByteArray* obj = v; + unsigned long long length = [self length]; + if (length != [obj length]) { + printf("Lengths differ: %llu versus %llu\n", length, [obj length]); + abort(); + return NO; + } + + unsigned long long offset; + unsigned char buffer1[1024]; + unsigned char buffer2[sizeof buffer1 / sizeof *buffer1]; + for (offset = 0; offset < length; offset += sizeof buffer1) { + memset(buffer1, 0, sizeof buffer1); + memset(buffer2, 0, sizeof buffer2); + size_t amountToGrab = sizeof buffer1; + if (amountToGrab > length - offset) amountToGrab = ll2l(length - offset); + [self copyBytes:buffer1 range:HFRangeMake(offset, amountToGrab)]; + [obj copyBytes:buffer2 range:HFRangeMake(offset, amountToGrab)]; + size_t i; + for (i=0; i < amountToGrab; i++) { + if (buffer1[i] != buffer2[i]) { + printf("Inconsistency found at %llu (%02x versus %02x)\n", i + offset, buffer1[i], buffer2[i]); + abort(); + return NO; + } + } + } + return YES; +} + +- (NSData *)_debugData { + NSMutableData *data = [NSMutableData dataWithLength:(NSUInteger)[self length]]; + [self copyBytes:[data mutableBytes] range:HFRangeMake(0, [self length])]; + return data; +} + +- (BOOL)_debugIsEqualToData:(NSData *)val { + REQUIRE_NOT_NULL(val); + HFByteArray *byteArray = [[NSClassFromString(@"HFFullMemoryByteArray") alloc] init]; + HFByteSlice *byteSlice = [[HFFullMemoryByteSlice alloc] initWithData:val]; + [byteArray insertByteSlice:byteSlice inRange:HFRangeMake(0, 0)]; + [byteSlice release]; + BOOL result = [self _debugIsEqual:byteArray]; + [byteArray release]; + return result; +} + +- (void)incrementChangeLockCounter { + [self willChangeValueForKey:@"changesAreLocked"]; + if (HFAtomicIncrement(&changeLockCounter, NO) == 0) { + [NSException raise:NSInvalidArgumentException format:@"change lock counter overflow for %@", self]; + } + [self didChangeValueForKey:@"changesAreLocked"]; +} + +- (void)decrementChangeLockCounter { + [self willChangeValueForKey:@"changesAreLocked"]; + if (HFAtomicDecrement(&changeLockCounter, NO) == NSUIntegerMax) { + [NSException raise:NSInvalidArgumentException format:@"change lock counter underflow for %@", self]; + } + [self didChangeValueForKey:@"changesAreLocked"]; +} + +- (BOOL)changesAreLocked { + return !! changeLockCounter; +} + +- (NSUInteger)changeGenerationCount { + return changeGenerationCount; +} + +- (void)incrementGenerationOrRaiseIfLockedForSelector:(SEL)sel { + if (changeLockCounter) { + [NSException raise:NSInvalidArgumentException format:@"Selector %@ sent to a locked byte array %@", NSStringFromSelector(sel), self]; + } + else { + HFAtomicIncrement(&changeGenerationCount, YES); + } +} + +@end diff --git a/bsnes/gb/HexFiend/HFByteArray_Internal.h b/bsnes/gb/HexFiend/HFByteArray_Internal.h new file mode 100644 index 00000000..648dd92c --- /dev/null +++ b/bsnes/gb/HexFiend/HFByteArray_Internal.h @@ -0,0 +1,8 @@ +#import + +@interface HFByteArray (HFInternal) + +- (BOOL)_debugIsEqual:(HFByteArray *)val; +- (BOOL)_debugIsEqualToData:(NSData *)val; + +@end diff --git a/bsnes/gb/HexFiend/HFByteSlice.h b/bsnes/gb/HexFiend/HFByteSlice.h new file mode 100644 index 00000000..b17da08d --- /dev/null +++ b/bsnes/gb/HexFiend/HFByteSlice.h @@ -0,0 +1,53 @@ +// +// HFByteSlice.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +@class HFFileReference, HFByteRangeAttributeArray; + +/*! @class HFByteSlice +@brief A class representing a source of data for an HFByteArray. + +HFByteSlice is an abstract class encapsulating primitive data sources (files, memory buffers, etc.). Each source must support random access reads, and have a well defined length. All HFByteSlices are \b immutable. + +The two principal subclasses of HFByteSlice are HFSharedMemoryByteSlice and HFFileByteSlice, which respectively encapsulate data from memory and from a file. +*/ +@interface HFByteSlice : NSObject { + NSUInteger retainCount; +} + +/*! Return the length of the byte slice as a 64 bit value. This is an abstract method that concrete subclasses must override. */ +- (unsigned long long)length; + +/*! Copies a range of data from the byte slice into an in-memory buffer. This is an abstract method that concrete subclasses must override. */ +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range; + +/*! Returns a new slice containing a subrange of the given slice. This is an abstract method that concrete subclasses must override. */ +- (HFByteSlice *)subsliceWithRange:(HFRange)range; + +/*! Attempts to create a new byte slice by appending one byte slice to another. This does not modify the receiver or the slice argument (after all, both are immutable). This is provided as an optimization, and is allowed to return nil if the appending cannot be done efficiently. The default implementation returns nil. +*/ +- (HFByteSlice *)byteSliceByAppendingSlice:(HFByteSlice *)slice; + +/*! Returns YES if the receiver is sourced from a file. The default implementation returns NO. This is used to estimate cost when writing to a file. +*/ +- (BOOL)isSourcedFromFile; + +/*! For a given file reference, returns the range within the file that the receiver is sourced from. If the receiver is not sourced from this file, returns {ULLONG_MAX, ULLONG_MAX}. The default implementation returns {ULLONG_MAX, ULLONG_MAX}. This is used during file saving to to determine how to properly overwrite a given file. +*/ +- (HFRange)sourceRangeForFile:(HFFileReference *)reference; + +@end + +/*! @category HFByteSlice(HFAttributes) + @brief Methods for querying attributes of individual byte slices. */ +@interface HFByteSlice (HFAttributes) + +/*! Returns the attributes for the bytes in the given range. */ +- (HFByteRangeAttributeArray *)attributesForBytesInRange:(HFRange)range; + +@end diff --git a/bsnes/gb/HexFiend/HFByteSlice.m b/bsnes/gb/HexFiend/HFByteSlice.m new file mode 100644 index 00000000..fadb442a --- /dev/null +++ b/bsnes/gb/HexFiend/HFByteSlice.m @@ -0,0 +1,85 @@ +// +// HFByteSlice.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + + +@implementation HFByteSlice + +- (instancetype)init { + if ([self class] == [HFByteSlice class]) { + [NSException raise:NSInvalidArgumentException format:@"init sent to HFByteArray, but HFByteArray is an abstract class. Instantiate one of its subclasses instead."]; + } + return [super init]; +} + +- (unsigned long long)length { UNIMPLEMENTED(); } + +- (void)copyBytes:(unsigned char*)dst range:(HFRange)range { USE(dst); USE(range); UNIMPLEMENTED_VOID(); } + +- (HFByteSlice *)subsliceWithRange:(HFRange)range { USE(range); UNIMPLEMENTED(); } + +- (void)constructNewByteSlicesAboutRange:(HFRange)range first:(HFByteSlice **)first second:(HFByteSlice **)second { + const unsigned long long length = [self length]; + + //clip the range to our extent + range.location = llmin(range.location, length); + range.length = llmin(range.length, length - range.location); + + HFRange firstRange = {0, range.location}; + HFRange secondRange = {range.location + range.length, [self length] - (range.location + range.length)}; + + if (first) { + if (firstRange.length > 0) + *first = [self subsliceWithRange:firstRange]; + else + *first = nil; + } + + if (second) { + if (secondRange.length > 0) + *second = [self subsliceWithRange:secondRange]; + else + *second = nil; + } +} + +- (HFByteSlice *)byteSliceByAppendingSlice:(HFByteSlice *)slice { + USE(slice); + return nil; +} + +- (HFByteRangeAttributeArray *)attributesForBytesInRange:(HFRange)range { + USE(range); + return nil; +} + +- (BOOL)isSourcedFromFile { + return NO; +} + +- (HFRange)sourceRangeForFile:(HFFileReference *)reference { + USE(reference); + return HFRangeMake(ULLONG_MAX, ULLONG_MAX); +} + +- (id)retain { + HFAtomicIncrement(&retainCount, NO); + return self; +} + +- (oneway void)release { + if (HFAtomicDecrement(&retainCount, NO) == (NSUInteger)(-1)) { + [self dealloc]; + } +} + +- (NSUInteger)retainCount { + return 1 + retainCount; +} + +@end diff --git a/bsnes/gb/HexFiend/HFByteSlice_Private.h b/bsnes/gb/HexFiend/HFByteSlice_Private.h new file mode 100644 index 00000000..2827bfd3 --- /dev/null +++ b/bsnes/gb/HexFiend/HFByteSlice_Private.h @@ -0,0 +1,7 @@ +#import + +@interface HFByteSlice (HFByteSlice_Private) + +- (void)constructNewByteSlicesAboutRange:(HFRange)range first:(HFByteSlice **)first second:(HFByteSlice **)second; + +@end diff --git a/bsnes/gb/HexFiend/HFController.h b/bsnes/gb/HexFiend/HFController.h new file mode 100644 index 00000000..4a59a4d6 --- /dev/null +++ b/bsnes/gb/HexFiend/HFController.h @@ -0,0 +1,395 @@ +// +// HFController.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +#import + +/*! @header HFController + @abstract The HFController.h header contains the HFController class, which is a central class in Hex Fiend. +*/ + +@class HFRepresenter, HFByteArray, HFFileReference, HFControllerCoalescedUndo, HFByteRangeAttributeArray; + +/*! @enum HFControllerPropertyBits + The HFControllerPropertyBits bitmask is used to inform the HFRepresenters of a change in the current state that they may need to react to. A bitmask of the changed properties is passed to representerChangedProperties:. It is common for multiple properties to be included in such a bitmask. +*/ +typedef NS_OPTIONS(NSUInteger, HFControllerPropertyBits) { + HFControllerContentValue = 1 << 0, /*!< Indicates that the contents of the ByteArray has changed within the document. There is no indication as to what the change is. If redisplaying everything is expensive, Representers should cache their displayed data and compute any changes manually. */ + HFControllerContentLength = 1 << 1, /*!< Indicates that the length of the ByteArray has changed. */ + HFControllerDisplayedLineRange = 1 << 2, /*!< Indicates that the displayedLineRange property of the document has changed (e.g. the user scrolled). */ + HFControllerSelectedRanges = 1 << 3, /*!< Indicates that the selectedContentsRanges property of the document has changed (e.g. the user selected some other range). */ + HFControllerSelectionPulseAmount = 1 << 4, /*!< Indicates that the amount of "pulse" to show in the Find pulse indicator has changed. */ + HFControllerBytesPerLine = 1 << 5, /*!< Indicates that the number of bytes to show per line has changed. */ + HFControllerBytesPerColumn = 1 << 6, /*!< Indicates that the number of bytes per column (byte grouping) has changed. */ + HFControllerEditable = 1 << 7, /*!< Indicates that the document has become (or is no longer) editable. */ + HFControllerFont = 1 << 8, /*!< Indicates that the font property has changed. */ + HFControllerAntialias = 1 << 9, /*!< Indicates that the shouldAntialias property has changed. */ + HFControllerLineHeight = 1 << 10, /*!< Indicates that the lineHeight property has changed. */ + HFControllerViewSizeRatios = 1 << 11, /*!< Indicates that the optimum size for each view may have changed; used by HFLayoutController after font changes. */ + HFControllerByteRangeAttributes = 1 << 12, /*!< Indicates that some attributes of the ByteArray has changed within the document. There is no indication as to what the change is. */ + HFControllerByteGranularity = 1 << 13, /*!< Indicates that the byte granularity has changed. For example, when moving from ASCII to UTF-16, the byte granularity increases from 1 to 2. */ + HFControllerBookmarks = 1 << 14, /*!< Indicates that a bookmark has been added or removed. */ + HFControllerColorBytes = 1 << 15, /*!< Indicates that the shouldColorBytes property has changed. */ + HFControllerShowCallouts = 1 << 16, /*!< Indicates that the shouldShowCallouts property has changed. */ + HFControllerHideNullBytes = 1 << 17, /*!< Indicates that the shouldHideNullBytes property has changed. */ +}; + +/*! @enum HFControllerMovementDirection + +The HFControllerMovementDirection enum is used to specify a direction (either left or right) in various text editing APIs. HexFiend does not support left-to-right languages. +*/ +typedef NS_ENUM(NSInteger, HFControllerMovementDirection) { + HFControllerDirectionLeft, + HFControllerDirectionRight +}; + +/*! @enum HFControllerSelectionTransformation + +The HFControllerSelectionTransformation enum is used to specify what happens to the selection in various APIs. This is mainly interesting for text-editing style Representers. +*/ +typedef NS_ENUM(NSInteger, HFControllerSelectionTransformation) { + HFControllerDiscardSelection, /*!< The selection should be discarded. */ + HFControllerShiftSelection, /*!< The selection should be moved, without changing its length. */ + HFControllerExtendSelection /*!< The selection should be extended, changing its length. */ +}; + +/*! @enum HFControllerMovementGranularity + +The HFControllerMovementGranularity enum is used to specify the granularity of text movement in various APIs. This is mainly interesting for text-editing style Representers. +*/ +typedef NS_ENUM(NSInteger, HFControllerMovementGranularity) { + HFControllerMovementByte, /*!< Move by individual bytes */ + HFControllerMovementColumn, /*!< Move by a column */ + HFControllerMovementLine, /*!< Move by lines */ + HFControllerMovementPage, /*!< Move by pages */ + HFControllerMovementDocument /*!< Move by the whole document */ +}; + +/*! @enum HFEditMode + +HFEditMode enumerates the different edit modes that a document might be in. + */ +typedef NS_ENUM(NSInteger, HFEditMode) { + HFInsertMode, + HFOverwriteMode, + HFReadOnlyMode, +} ; + +/*! @class HFController +@brief A central class that acts as the controller layer for HexFiend.framework + +HFController acts as the controller layer in the MVC architecture of HexFiend. The HFController plays several significant central roles, including: + - Mediating between the data itself (in the HFByteArray) and the views of the data (the @link HFRepresenter HFRepresenters@endlink). + - Propagating changes to the views. + - Storing properties common to all Representers, such as the currently diplayed range, the currently selected range(s), the font, etc. + - Handling text editing actions, such as selection changes or insertions/deletions. + +An HFController is the top point of ownership for a HexFiend object graph. It retains both its ByteArray (model) and its array of Representers (views). + +You create an HFController via [[HFController alloc] init]. After that, give it an HFByteArray via setByteArray:, and some Representers via addRepresenter:. Then insert the Representers' views in a window, and you're done. + +*/ +@interface HFController : NSObject { +@private + NSMutableArray *representers; + HFByteArray *byteArray; + NSMutableArray *selectedContentsRanges; + HFRange displayedContentsRange; + HFFPRange displayedLineRange; + NSUInteger bytesPerLine; + NSUInteger bytesPerColumn; + CGFloat lineHeight; + + NSUInteger currentPropertyChangeToken; + NSMutableArray *additionalPendingTransactions; + HFControllerPropertyBits propertiesToUpdateInCurrentTransaction; + + NSUndoManager *undoManager; + NSMutableSet *undoOperations; + HFControllerCoalescedUndo *undoCoalescer; + + unsigned long long selectionAnchor; + HFRange selectionAnchorRange; + + CFAbsoluteTime pulseSelectionStartTime, pulseSelectionCurrentTime; + NSTimer *pulseSelectionTimer; + + /* Basic cache support */ + HFRange cachedRange; + NSData *cachedData; + NSUInteger cachedGenerationIndex; + + struct { + unsigned antialias:1; + unsigned colorbytes:1; + unsigned showcallouts:1; + unsigned hideNullBytes:1; + HFEditMode editMode:2; + unsigned editable:1; + unsigned selectable:1; + unsigned selectionInProgress:1; + unsigned shiftExtendSelection:1; + unsigned commandExtendSelection:1; + unsigned livereload:1; + } _hfflags; +} + +/*! @name Representer handling. + Methods for modifying the list of HFRepresenters attached to a controller. Attached representers receive the controllerDidChange: message when various properties of the controller change. A representer may only be attached to one controller at a time. Representers are retained by the controller. +*/ +//@{ +/// Gets the current array of representers attached to this controller. +@property (readonly, copy) NSArray *representers; + +/// Adds a new representer to this controller. +- (void)addRepresenter:(HFRepresenter *)representer; + +/// Removes an existing representer from this controller. The representer must be present in the array of representers. +- (void)removeRepresenter:(HFRepresenter *)representer; + +//@} + +/*! @name Property transactions + Methods for temporarily delaying notifying representers of property changes. There is a property transaction stack, and all property changes are collected until the last token is popped off the stack, at which point all representers are notified of all collected changes via representerChangedProperties:. To use this, call beginPropertyChangeTransaction, and record the token that is returned. Pass it to endPropertyChangeTransaction: to notify representers of all changed properties in bulk. + + Tokens cannot be popped out of order - they are used only as a correctness check. +*/ +//@{ +/*! Begins delaying property change transactions. Returns a token that should be passed to endPropertyChangeTransactions:. */ +- (NSUInteger)beginPropertyChangeTransaction; + +/*! Pass a token returned from beginPropertyChangeTransaction to this method to pop the transaction off the stack and, if the stack is empty, to notify Representers of all collected changes. Tokens cannot be popped out of order - they are used strictly as a correctness check. */ +- (void)endPropertyChangeTransaction:(NSUInteger)token; +//@} + +/*! @name Byte array + Set and get the byte array. */ +//@{ + +/*! The byte array must be non-nil. In general, HFRepresenters should not use this to determine what bytes to display. Instead they should use copyBytes:range: or dataForRange: below. */ +@property (nonatomic, strong) HFByteArray *byteArray; + +/*! Replaces the entire byte array with a new one, preserving as much of the selection as possible. Unlike setByteArray:, this method is undoable, and intended to be used from representers that make a global change (such as Replace All). */ +- (void)replaceByteArray:(HFByteArray *)newArray; +//@} + +/*! @name Properties shared between all representers + The following properties are considered global among all HFRepresenters attached to the receiver. +*/ +//@{ +/*! Returns the number of lines on which the cursor may be placed. This is always at least 1, and is equivalent to (unsigned long long)(HFRoundUpToNextMultiple(contentsLength, bytesPerLine) / bytesPerLine) */ +- (unsigned long long)totalLineCount; + +/*! Indicates the number of bytes per line, which is a global property among all the line-oriented representers. */ +- (NSUInteger)bytesPerLine; + +/*! Returns the height of a line, in points. This is generally determined by the font. Representers that wish to align things to lines should use this. */ +- (CGFloat)lineHeight; + +//@} + +/*! @name Selection pulsing + Used to show the current selection after a change, similar to Find in Safari +*/ +//{@ + +/*! Begins selection pulsing (e.g. following a successful Find operation). Representers will receive callbacks indicating that HFControllerSelectionPulseAmount has changed. */ +- (void)pulseSelection; + +/*! Return the amount that the "Find pulse indicator" should show. 0 means no pulse, 1 means maximum pulse. This is useful for Representers that support find and replace. */ +- (double)selectionPulseAmount; +//@} + +/*! @name Selection handling + Methods for manipulating the current selected ranges. Hex Fiend supports discontiguous selection. +*/ +//{@ + +/*! An array of HFRangeWrappers, representing the selected ranges. It satisfies the following: + The array is non-nil. + There always is at least one selected range. + If any range has length 0, that range is the only range. + No range extends beyond the contentsLength, with the exception of a single zero-length range at the end. + + When setting, the setter MUST obey the above criteria. A zero length range when setting or getting represents the cursor position. */ +@property (nonatomic, copy) NSArray *selectedContentsRanges; + +/*! Selects the entire contents. */ +- (IBAction)selectAll:(id)sender; + +/*! Returns the smallest value in the selected contents ranges, or the insertion location if the selection is empty. */ +- (unsigned long long)minimumSelectionLocation; + +/*! Returns the largest HFMaxRange of the selected contents ranges, or the insertion location if the selection is empty. */ +- (unsigned long long)maximumSelectionLocation; + +/*! Convenience method for creating a byte array containing all of the selected bytes. If the selection has length 0, this returns an empty byte array. */ +- (HFByteArray *)byteArrayForSelectedContentsRanges; +//@} + +/* Number of bytes used in each column for a text-style representer. */ +@property (nonatomic) NSUInteger bytesPerColumn; + +/*! @name Edit Mode + Determines what mode we're in, read-only, overwrite or insert. */ +@property (nonatomic) HFEditMode editMode; + +/*! @name Displayed line range + Methods for setting and getting the current range of displayed lines. +*/ +//{@ +/*! Get the current displayed line range. The displayed line range is an HFFPRange (range of long doubles) containing the lines that are currently displayed. + + The values may be fractional. That is, if only the bottom half of line 4 through the top two thirds of line 8 is shown, then the displayedLineRange.location will be 4.5 and the displayedLineRange.length will be 3.17 ( = 7.67 - 4.5). Representers are expected to be able to handle such fractional values. + + When setting the displayed line range, the given range must be nonnegative, and the maximum of the range must be no larger than the total line count. + +*/ +@property (nonatomic) HFFPRange displayedLineRange; + +/*! Modify the displayedLineRange so that as much of the given range as can fit is visible. If possible, moves by as little as possible so that the visible ranges before and afterward intersect with each other. */ +- (void)maximizeVisibilityOfContentsRange:(HFRange)range; + +/*! Modify the displayedLineRange as to center the given contents range. If the range is near the bottom or top, this will center as close as possible. If contents range is too large to fit, it centers the top of the range. contentsRange may be empty. */ +- (void)centerContentsRange:(HFRange)range; + +//@} + +/*! The current font. */ +@property (nonatomic, copy) NSFont *font; + +/*! The undo manager. If no undo manager is set, then undo is not supported. By default the undo manager is nil. +*/ +@property (nonatomic, strong) NSUndoManager *undoManager; + +/*! Whether the user can edit the document. */ +@property (nonatomic) BOOL editable; + +/*! Whether the text should be antialiased. Note that Mac OS X settings may prevent antialiasing text below a certain point size. */ +@property (nonatomic) BOOL shouldAntialias; + +/*! When enabled, characters have a background color that correlates to their byte values. */ +@property (nonatomic) BOOL shouldColorBytes; + +/*! When enabled, byte bookmarks display callout-style labels attached to them. */ +@property (nonatomic) BOOL shouldShowCallouts; + +/*! When enabled, null bytes are hidden in the hex view. */ +@property (nonatomic) BOOL shouldHideNullBytes; + +/*! When enabled, unmodified documents are auto refreshed to their latest on disk state. */ +@property (nonatomic) BOOL shouldLiveReload; + +/*! Representer initiated property changes + Called from a representer to indicate when some internal property of the representer has changed which requires that some properties be recalculated. +*/ +//@{ +/*! Callback for a representer-initiated change to some property. For example, if some property of a view changes that would cause the number of bytes per line to change, then the representer should call this method which will trigger the HFController to recompute the relevant properties. */ + +- (void)representer:(HFRepresenter *)rep changedProperties:(HFControllerPropertyBits)properties; +//@} + +/*! @name Mouse selection + Methods to handle mouse selection. Representers that allow text selection should call beginSelectionWithEvent:forByteIndex: upon receiving a mouseDown event, and then continueSelectionWithEvent:forByteIndex: for mouseDragged events, terminating with endSelectionWithEvent:forByteIndex: upon receiving the mouse up. HFController will compute the correct selected ranges and propagate any changes via the HFControllerPropertyBits mechanism. */ +//@{ +/*! Begin a selection session, with a mouse down at the given byte index. */ +- (void)beginSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex; + +/*! Continue a selection session, whe the user drags over the given byte index. */ +- (void)continueSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex; + +/*! End a selection session, with a mouse up at the given byte index. */ +- (void)endSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex; + +/*! @name Scrollling + Support for the mouse wheel and scroll bars. */ +//@{ +/*! Trigger scrolling appropriate for the given scroll event. */ +- (void)scrollWithScrollEvent:(NSEvent *)scrollEvent; + +/*! Trigger scrolling by the given number of lines. If lines is positive, then the document is scrolled down; otherwise it is scrolled up. */ +- (void)scrollByLines:(long double)lines; + +//@} + +/*! @name Keyboard navigation + Support for chaging the selection via the keyboard +*/ + +/*! General purpose navigation function. Modify the selection in the given direction by the given number of bytes. The selection is modifed according to the given transformation. If useAnchor is set, then anchored selection is used; otherwise any anchor is discarded. + + This has a few limitations: + - Only HFControllerDirectionLeft and HFControllerDirectionRight movement directions are supported. + - Anchored selection is not supported for HFControllerShiftSelection (useAnchor must be NO) +*/ +- (void)moveInDirection:(HFControllerMovementDirection)direction byByteCount:(unsigned long long)amountToMove withSelectionTransformation:(HFControllerSelectionTransformation)transformation usingAnchor:(BOOL)useAnchor; + +/*! Navigation designed for key events. */ +- (void)moveInDirection:(HFControllerMovementDirection)direction withGranularity:(HFControllerMovementGranularity)granularity andModifySelection:(BOOL)extendSelection; +- (void)moveToLineBoundaryInDirection:(HFControllerMovementDirection)direction andModifySelection:(BOOL)extendSelection; + +/*! @name Text editing + Methods to support common text editing operations */ +//@{ + +/*! Replaces the selection with the given data. For something like a hex view representer, it takes two keypresses to create a whole byte; the way this is implemented, the first keypress goes into the data as a complete byte, and the second one (if any) replaces it. If previousByteCount > 0, then that many prior bytes are replaced, without breaking undo coalescing. For previousByteCount to be > 0, the following must be true: There is only one selected range, and it is of length 0, and its location >= previousByteCount + + These functions return YES if they succeed, and NO if they fail. Currently they may fail only in overwrite mode, if you attempt to insert data that would require lengthening the byte array. + + These methods are undoable. + */ +- (BOOL)insertByteArray:(HFByteArray *)byteArray replacingPreviousBytes:(unsigned long long)previousByteCount allowUndoCoalescing:(BOOL)allowUndoCoalescing; +- (BOOL)insertData:(NSData *)data replacingPreviousBytes:(unsigned long long)previousByteCount allowUndoCoalescing:(BOOL)allowUndoCoalescing; + +/*! Deletes the selection. This operation is undoable. */ +- (void)deleteSelection; + +/*! If the selection is empty, deletes one byte in a given direction, which must be HFControllerDirectionLeft or HFControllerDirectionRight; if the selection is not empty, deletes the selection. Undoable. */ +- (void)deleteDirection:(HFControllerMovementDirection)direction; + +//@} + +/*! @name Reading data + Methods for reading data */ + +/*! Returns an NSData representing the given HFRange. The length of the HFRange must be of a size that can reasonably be fit in memory. This method may cache the result. */ +- (NSData *)dataForRange:(HFRange)range; + +/*! Copies data within the given HFRange into an in-memory buffer. This is equivalent to [[controller byteArray] copyBytes:bytes range:range]. */ +- (void)copyBytes:(unsigned char *)bytes range:(HFRange)range; + +/*! Returns total number of bytes. This is equivalent to [[controller byteArray] length]. */ +- (unsigned long long)contentsLength; + +- (void) reloadData; +- (void)_ensureVisibilityOfLocation:(unsigned long long)location; +@end + +/*! A notification posted whenever any of the HFController's properties change. The object is the HFController. The userInfo contains one key, HFControllerChangedPropertiesKey, which contains an NSNumber with the changed properties as a HFControllerPropertyBits bitmask. This is useful for external objects to be notified of changes. HFRepresenters added to the HFController are notified via the controllerDidChange: message. +*/ +extern NSString * const HFControllerDidChangePropertiesNotification; + +/*! @name HFControllerDidChangePropertiesNotification keys +*/ +//@{ +extern NSString * const HFControllerChangedPropertiesKey; //!< A key in the HFControllerDidChangeProperties containing a bitmask of the changed properties, as a HFControllerPropertyBits +//@} + +/*! A notification posted from prepareForChangeInFile:fromWritingByteArray: because we are about to write a ByteArray to a file. The object is the FileReference. + Currently, HFControllers do not listen for this notification. This is because under GC there is no way of knowing whether the controller is live or not. However, pasteboard owners do listen for it, because as long as we own a pasteboard we are guaranteed to be live. +*/ +extern NSString * const HFPrepareForChangeInFileNotification; + +/*! @name HFPrepareForChangeInFileNotification keys +*/ +//@{ +extern NSString * const HFChangeInFileByteArrayKey; //!< A key in the HFPrepareForChangeInFileNotification specifying the byte array that will be written +extern NSString * const HFChangeInFileModifiedRangesKey; //!< A key in the HFPrepareForChangeInFileNotification specifying the array of HFRangeWrappers indicating which parts of the file will be modified +extern NSString * const HFChangeInFileShouldCancelKey; //!< A key in the HFPrepareForChangeInFileNotification specifying an NSValue containing a pointer to a BOOL. If set to YES, then someone was unable to prepare and the file should not be saved. It's a good idea to check if this value points to YES; if so your notification handler does not have to do anything. +extern NSString * const HFChangeInFileHintKey; //!< The hint parameter that you may pass to clearDependenciesOnRanges:inFile:hint: +//@} diff --git a/bsnes/gb/HexFiend/HFController.m b/bsnes/gb/HexFiend/HFController.m new file mode 100644 index 00000000..74e033c0 --- /dev/null +++ b/bsnes/gb/HexFiend/HFController.m @@ -0,0 +1,1900 @@ +// +// HFController.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +/* Used for the anchor range and location */ +#define NO_SELECTION ULLONG_MAX + +#define VALIDATE_SELECTION() [self _ensureSelectionIsValid] + +#define BENCHMARK_BYTEARRAYS 0 + +#define BEGIN_TRANSACTION() NSUInteger token = [self beginPropertyChangeTransaction] +#define END_TRANSACTION() [self endPropertyChangeTransaction:token] + +static const CGFloat kScrollMultiplier = (CGFloat)1.5; + +static const CFTimeInterval kPulseDuration = .2; + +static void *KVOContextChangesAreLocked = &KVOContextChangesAreLocked; + +NSString * const HFPrepareForChangeInFileNotification = @"HFPrepareForChangeInFileNotification"; +NSString * const HFChangeInFileByteArrayKey = @"HFChangeInFileByteArrayKey"; +NSString * const HFChangeInFileModifiedRangesKey = @"HFChangeInFileModifiedRangesKey"; +NSString * const HFChangeInFileShouldCancelKey = @"HFChangeInFileShouldCancelKey"; +NSString * const HFChangeInFileHintKey = @"HFChangeInFileHintKey"; + +NSString * const HFControllerDidChangePropertiesNotification = @"HFControllerDidChangePropertiesNotification"; +NSString * const HFControllerChangedPropertiesKey = @"HFControllerChangedPropertiesKey"; + + +typedef NS_ENUM(NSInteger, HFControllerSelectAction) { + eSelectResult, + eSelectAfterResult, + ePreserveSelection, + NUM_SELECTION_ACTIONS +}; + +@interface HFController (ForwardDeclarations) +- (void)_commandInsertByteArrays:(NSArray *)byteArrays inRanges:(NSArray *)ranges withSelectionAction:(HFControllerSelectAction)selectionAction; +- (void)_removeUndoManagerNotifications; +- (void)_removeAllUndoOperations; +- (void)_registerUndoOperationForInsertingByteArrays:(NSArray *)byteArrays inRanges:(NSArray *)ranges withSelectionAction:(HFControllerSelectAction)selectionAction; + +- (void)_updateBytesPerLine; +- (void)_updateDisplayedRange; +@end + +@interface NSEvent (HFLionStuff) +- (CGFloat)scrollingDeltaY; +- (BOOL)hasPreciseScrollingDeltas; +- (CGFloat)deviceDeltaY; +@end + +static inline Class preferredByteArrayClass(void) { + return [HFBTreeByteArray class]; +} + +@implementation HFController + +- (void)_sharedInit { + selectedContentsRanges = [[NSMutableArray alloc] initWithObjects:[HFRangeWrapper withRange:HFRangeMake(0, 0)], nil]; + byteArray = [[preferredByteArrayClass() alloc] init]; + [byteArray addObserver:self forKeyPath:@"changesAreLocked" options:0 context:KVOContextChangesAreLocked]; + selectionAnchor = NO_SELECTION; + undoOperations = [[NSMutableSet alloc] init]; +} + +- (instancetype)init { + self = [super init]; + [self _sharedInit]; + bytesPerLine = 16; + bytesPerColumn = 1; + _hfflags.editable = YES; + _hfflags.antialias = YES; + _hfflags.showcallouts = YES; + _hfflags.hideNullBytes = NO; + _hfflags.selectable = YES; + representers = [[NSMutableArray alloc] init]; + [self setFont:[NSFont fontWithName:HFDEFAULT_FONT size:HFDEFAULT_FONTSIZE]]; + return self; +} + +- (void)dealloc { + [representers makeObjectsPerformSelector:@selector(_setController:) withObject:nil]; + [representers release]; + [selectedContentsRanges release]; + [self _removeUndoManagerNotifications]; + [self _removeAllUndoOperations]; + [undoOperations release]; + [undoManager release]; + [undoCoalescer release]; + [_font release]; + [byteArray removeObserver:self forKeyPath:@"changesAreLocked"]; + [byteArray release]; + [cachedData release]; + [additionalPendingTransactions release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [coder encodeObject:representers forKey:@"HFRepresenters"]; + [coder encodeInt64:bytesPerLine forKey:@"HFBytesPerLine"]; + [coder encodeInt64:bytesPerColumn forKey:@"HFBytesPerColumn"]; + [coder encodeObject:_font forKey:@"HFFont"]; + [coder encodeDouble:lineHeight forKey:@"HFLineHeight"]; + [coder encodeBool:_hfflags.antialias forKey:@"HFAntialias"]; + [coder encodeBool:_hfflags.colorbytes forKey:@"HFColorBytes"]; + [coder encodeBool:_hfflags.showcallouts forKey:@"HFShowCallouts"]; + [coder encodeBool:_hfflags.hideNullBytes forKey:@"HFHidesNullBytes"]; + [coder encodeBool:_hfflags.livereload forKey:@"HFLiveReload"]; + [coder encodeInt:_hfflags.editMode forKey:@"HFEditMode"]; + [coder encodeBool:_hfflags.editable forKey:@"HFEditable"]; + [coder encodeBool:_hfflags.selectable forKey:@"HFSelectable"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super init]; + [self _sharedInit]; + bytesPerLine = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesPerLine"]; + bytesPerColumn = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesPerColumn"]; + _font = [[coder decodeObjectForKey:@"HFFont"] retain]; + lineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFLineHeight"]; + _hfflags.antialias = [coder decodeBoolForKey:@"HFAntialias"]; + _hfflags.colorbytes = [coder decodeBoolForKey:@"HFColorBytes"]; + _hfflags.livereload = [coder decodeBoolForKey:@"HFLiveReload"]; + + if ([coder containsValueForKey:@"HFEditMode"]) + _hfflags.editMode = [coder decodeIntForKey:@"HFEditMode"]; + else { + _hfflags.editMode = ([coder decodeBoolForKey:@"HFOverwriteMode"] + ? HFOverwriteMode : HFInsertMode); + } + + _hfflags.editable = [coder decodeBoolForKey:@"HFEditable"]; + _hfflags.selectable = [coder decodeBoolForKey:@"HFSelectable"]; + _hfflags.hideNullBytes = [coder decodeBoolForKey:@"HFHidesNullBytes"]; + representers = [[coder decodeObjectForKey:@"HFRepresenters"] retain]; + return self; +} + +- (NSArray *)representers { + return [[representers copy] autorelease]; +} + +- (void)notifyRepresentersOfChanges:(HFControllerPropertyBits)bits { + FOREACH(HFRepresenter*, rep, representers) { + [rep controllerDidChange:bits]; + } + + /* Post the HFControllerDidChangePropertiesNotification */ + NSNumber *number = [[NSNumber alloc] initWithUnsignedInteger:bits]; + NSDictionary *userInfo = [[NSDictionary alloc] initWithObjects:&number forKeys:(id *)&HFControllerChangedPropertiesKey count:1]; + [number release]; + [[NSNotificationCenter defaultCenter] postNotificationName:HFControllerDidChangePropertiesNotification object:self userInfo:userInfo]; + [userInfo release]; +} + +- (void)_firePropertyChanges { + NSMutableArray *pendingTransactions = additionalPendingTransactions; + NSUInteger pendingTransactionCount = [pendingTransactions count]; + additionalPendingTransactions = nil; + HFControllerPropertyBits propertiesToUpdate = propertiesToUpdateInCurrentTransaction; + propertiesToUpdateInCurrentTransaction = 0; + if (pendingTransactionCount > 0 || propertiesToUpdate != 0) { + BEGIN_TRANSACTION(); + while (pendingTransactionCount--) { + HFControllerPropertyBits propertiesInThisTransaction = [pendingTransactions[0] unsignedIntegerValue]; + [pendingTransactions removeObjectAtIndex:0]; + HFASSERT(propertiesInThisTransaction != 0); + [self notifyRepresentersOfChanges:propertiesInThisTransaction]; + } + [pendingTransactions release]; + if (propertiesToUpdate) { + [self notifyRepresentersOfChanges:propertiesToUpdate]; + } + END_TRANSACTION(); + } +} + +/* Inserts a "fence" so that all prior property change bits will be complete before any new ones */ +- (void)_insertPropertyChangeFence { + if (currentPropertyChangeToken == 0) { + HFASSERT(additionalPendingTransactions == nil); + /* There can be no prior property changes */ + HFASSERT(propertiesToUpdateInCurrentTransaction == 0); + return; + } + if (propertiesToUpdateInCurrentTransaction == 0) { + /* Nothing to fence */ + return; + } + if (additionalPendingTransactions == nil) additionalPendingTransactions = [[NSMutableArray alloc] init]; + [additionalPendingTransactions addObject:@(propertiesToUpdateInCurrentTransaction)]; + propertiesToUpdateInCurrentTransaction = 0; +} + +- (void)_addPropertyChangeBits:(HFControllerPropertyBits)bits { + propertiesToUpdateInCurrentTransaction |= bits; + if (currentPropertyChangeToken == 0) { + [self _firePropertyChanges]; + } +} + +- (NSUInteger)beginPropertyChangeTransaction { + HFASSERT(currentPropertyChangeToken < NSUIntegerMax); + return ++currentPropertyChangeToken; +} + +- (void)endPropertyChangeTransaction:(NSUInteger)token { + if (currentPropertyChangeToken != token) { + [NSException raise:NSInvalidArgumentException format:@"endPropertyChangeTransaction passed token %lu, but expected token %lu", (unsigned long)token, (unsigned long)currentPropertyChangeToken]; + } + HFASSERT(currentPropertyChangeToken > 0); + if (--currentPropertyChangeToken == 0) [self _firePropertyChanges]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (context == KVOContextChangesAreLocked) { + HFASSERT([keyPath isEqual:@"changesAreLocked"]); + [self _addPropertyChangeBits:HFControllerEditable]; + } + else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)addRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + HFASSERT([representers indexOfObjectIdenticalTo:representer] == NSNotFound); + HFASSERT([representer controller] == nil); + [representer _setController:self]; + [representers addObject:representer]; + [representer controllerDidChange: -1]; +} + +- (void)removeRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + HFASSERT([representers indexOfObjectIdenticalTo:representer] != NSNotFound); + [representers removeObjectIdenticalTo:representer]; + [representer _setController:nil]; +} + +- (HFRange)_maximumDisplayedRangeSet { + unsigned long long contentsLength = [self contentsLength]; + HFRange maximumDisplayedRangeSet = HFRangeMake(0, HFRoundUpToNextMultipleSaturate(contentsLength, bytesPerLine)); + return maximumDisplayedRangeSet; +} + +- (unsigned long long)totalLineCount { + return HFDivideULLRoundingUp(HFRoundUpToNextMultipleSaturate([self contentsLength] - 1, bytesPerLine), bytesPerLine); +} + +- (HFFPRange)displayedLineRange { +#if ! NDEBUG + HFASSERT(displayedLineRange.location >= 0); + HFASSERT(displayedLineRange.length >= 0); + HFASSERT(displayedLineRange.location + displayedLineRange.length <= HFULToFP([self totalLineCount])); +#endif + return displayedLineRange; +} + +- (void)setDisplayedLineRange:(HFFPRange)range { +#if ! NDEBUG + HFASSERT(range.location >= 0); + HFASSERT(range.length >= 0); + HFASSERT(range.location + range.length <= HFULToFP([self totalLineCount])); +#endif + if (! HFFPRangeEqualsRange(range, displayedLineRange)) { + displayedLineRange = range; + [self _addPropertyChangeBits:HFControllerDisplayedLineRange]; + } +} + +- (CGFloat)lineHeight { + return lineHeight; +} + +- (void)setFont:(NSFont *)val { + if (val != _font) { + CGFloat priorLineHeight = [self lineHeight]; + + [_font release]; + _font = [val copy]; + + NSLayoutManager *manager = [[NSLayoutManager alloc] init]; + lineHeight = [manager defaultLineHeightForFont:_font]; + [manager release]; + + HFControllerPropertyBits bits = HFControllerFont; + if (lineHeight != priorLineHeight) bits |= HFControllerLineHeight; + [self _addPropertyChangeBits:bits]; + [self _insertPropertyChangeFence]; + [self _addPropertyChangeBits:HFControllerViewSizeRatios]; + [self _updateDisplayedRange]; + } +} + +- (BOOL)shouldAntialias { + return _hfflags.antialias; +} + +- (void)setShouldAntialias:(BOOL)antialias { + antialias = !! antialias; + if (antialias != _hfflags.antialias) { + _hfflags.antialias = antialias; + [self _addPropertyChangeBits:HFControllerAntialias]; + } +} + +- (BOOL)shouldColorBytes { + return _hfflags.colorbytes; +} + +- (void)setShouldColorBytes:(BOOL)colorbytes { + colorbytes = !! colorbytes; + if (colorbytes != _hfflags.colorbytes) { + _hfflags.colorbytes = colorbytes; + [self _addPropertyChangeBits:HFControllerColorBytes]; + } +} + +- (BOOL)shouldShowCallouts { + return _hfflags.showcallouts; +} + +- (void)setShouldShowCallouts:(BOOL)showcallouts { + showcallouts = !! showcallouts; + if (showcallouts != _hfflags.showcallouts) { + _hfflags.showcallouts = showcallouts; + [self _addPropertyChangeBits:HFControllerShowCallouts]; + } +} + +- (BOOL)shouldHideNullBytes { + return _hfflags.hideNullBytes; +} + +- (void)setShouldHideNullBytes:(BOOL)hideNullBytes +{ + hideNullBytes = !! hideNullBytes; + if (hideNullBytes != _hfflags.hideNullBytes) { + _hfflags.hideNullBytes = hideNullBytes; + [self _addPropertyChangeBits:HFControllerHideNullBytes]; + } +} + +- (BOOL)shouldLiveReload { + return _hfflags.livereload; +} + +- (void)setShouldLiveReload:(BOOL)livereload { + _hfflags.livereload = !!livereload; + +} + +- (void)setBytesPerColumn:(NSUInteger)val { + if (val != bytesPerColumn) { + bytesPerColumn = val; + [self _addPropertyChangeBits:HFControllerBytesPerColumn]; + } +} + +- (NSUInteger)bytesPerColumn { + return bytesPerColumn; +} + +- (BOOL)_shouldInvertSelectedRangesByAnchorRange { + return _hfflags.selectionInProgress && _hfflags.commandExtendSelection; +} + +- (NSArray *)_invertedSelectedContentsRanges { + HFASSERT([selectedContentsRanges count] > 0); + HFASSERT(selectionAnchorRange.location != NO_SELECTION); + if (selectionAnchorRange.length == 0) return [NSArray arrayWithArray:selectedContentsRanges]; + + NSArray *cleanedRanges = [HFRangeWrapper organizeAndMergeRanges:selectedContentsRanges]; + NSMutableArray *result = [NSMutableArray array]; + + /* Our algorithm works as follows - add any ranges outside of the selectionAnchorRange, clipped by the selectionAnchorRange. Then extract every "index" in our cleaned selected arrays that are within the selectionAnchorArray. An index is the location where a range starts or stops. Then use those indexes to create the inverted arrays. A range parity of 1 means that we are adding the range. */ + + /* Add all the ranges that are outside of selectionAnchorRange, clipping them if necessary */ + HFASSERT(HFSumDoesNotOverflow(selectionAnchorRange.location, selectionAnchorRange.length)); + FOREACH(HFRangeWrapper*, outsideWrapper, cleanedRanges) { + HFRange range = [outsideWrapper HFRange]; + if (range.location < selectionAnchorRange.location) { + HFRange clippedRange; + clippedRange.location = range.location; + HFASSERT(MIN(HFMaxRange(range), selectionAnchorRange.location) >= clippedRange.location); + clippedRange.length = MIN(HFMaxRange(range), selectionAnchorRange.location) - clippedRange.location; + [result addObject:[HFRangeWrapper withRange:clippedRange]]; + } + if (HFMaxRange(range) > HFMaxRange(selectionAnchorRange)) { + HFRange clippedRange; + clippedRange.location = MAX(range.location, HFMaxRange(selectionAnchorRange)); + HFASSERT(HFMaxRange(range) >= clippedRange.location); + clippedRange.length = HFMaxRange(range) - clippedRange.location; + [result addObject:[HFRangeWrapper withRange:clippedRange]]; + } + } + + HFASSERT(HFSumDoesNotOverflow(selectionAnchorRange.location, selectionAnchorRange.length)); + + NEW_ARRAY(unsigned long long, partitions, 2*[cleanedRanges count] + 2); + NSUInteger partitionCount, partitionIndex = 0; + + partitions[partitionIndex++] = selectionAnchorRange.location; + FOREACH(HFRangeWrapper*, wrapper, cleanedRanges) { + HFRange range = [wrapper HFRange]; + if (! HFIntersectsRange(range, selectionAnchorRange)) continue; + + partitions[partitionIndex++] = MAX(selectionAnchorRange.location, range.location); + partitions[partitionIndex++] = MIN(HFMaxRange(selectionAnchorRange), HFMaxRange(range)); + } + + // For some reason, using HFMaxRange confuses the static analyzer + partitions[partitionIndex++] = HFSum(selectionAnchorRange.location, selectionAnchorRange.length); + + partitionCount = partitionIndex; + HFASSERT((partitionCount % 2) == 0); + + partitionIndex = 0; + while (partitionIndex < partitionCount) { + HFASSERT(partitionIndex + 1 < partitionCount); + HFASSERT(partitions[partitionIndex] <= partitions[partitionIndex + 1]); + if (partitions[partitionIndex] < partitions[partitionIndex + 1]) { + HFRange range = HFRangeMake(partitions[partitionIndex], partitions[partitionIndex + 1] - partitions[partitionIndex]); + [result addObject:[HFRangeWrapper withRange:range]]; + } + partitionIndex += 2; + } + + FREE_ARRAY(partitions); + + if ([result count] == 0) [result addObject:[HFRangeWrapper withRange:HFRangeMake(selectionAnchor, 0)]]; + + return [HFRangeWrapper organizeAndMergeRanges:result]; +} + +- (void)_ensureSelectionIsValid { + HFASSERT(selectedContentsRanges != nil); + HFASSERT([selectedContentsRanges count] > 0); + BOOL onlyOneWrapper = ([selectedContentsRanges count] == 1); + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + EXPECT_CLASS(wrapper, HFRangeWrapper); + HFRange range = [wrapper HFRange]; + if (!HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))){ + [self setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(0, 0)]]]; + return; + } + if (onlyOneWrapper == NO) HFASSERT(range.length > 0); /* If we have more than one wrapper, then none of them should be zero length */ + } +} + +- (void)_setSingleSelectedContentsRange:(HFRange)newSelection { + HFASSERT(HFRangeIsSubrangeOfRange(newSelection, HFRangeMake(0, [self contentsLength]))); + BOOL selectionChanged; + if ([selectedContentsRanges count] == 1) { + selectionChanged = ! HFRangeEqualsRange([selectedContentsRanges[0] HFRange], newSelection); + } + else { + selectionChanged = YES; + } + + if (selectionChanged) { + [selectedContentsRanges removeAllObjects]; + [selectedContentsRanges addObject:[HFRangeWrapper withRange:newSelection]]; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + VALIDATE_SELECTION(); +} + +- (NSArray *)selectedContentsRanges { + VALIDATE_SELECTION(); + if ([self _shouldInvertSelectedRangesByAnchorRange]) return [self _invertedSelectedContentsRanges]; + else return [NSArray arrayWithArray:selectedContentsRanges]; +} + +- (unsigned long long)contentsLength { + if (! byteArray) return 0; + else return [byteArray length]; +} + +- (NSData *)dataForRange:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); // it doesn't make sense to ask for a buffer larger than can be stored in memory + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + + if(range.length == 0) { + // Don't throw out cache for an empty request! Also makes the analyzer happier. + return [NSData data]; + } + + NSUInteger newGenerationIndex = [byteArray changeGenerationCount]; + if (cachedData == nil || newGenerationIndex != cachedGenerationIndex || ! HFRangeIsSubrangeOfRange(range, cachedRange)) { + [cachedData release]; + cachedGenerationIndex = newGenerationIndex; + cachedRange = range; + NSUInteger length = ll2l(range.length); + unsigned char *data = check_malloc(length); + [byteArray copyBytes:data range:range]; + cachedData = [[NSData alloc] initWithBytesNoCopy:data length:length freeWhenDone:YES]; + } + + if (HFRangeEqualsRange(range, cachedRange)) { + return cachedData; + } + else { + HFASSERT(cachedRange.location <= range.location); + NSRange cachedDataSubrange; + cachedDataSubrange.location = ll2l(range.location - cachedRange.location); + cachedDataSubrange.length = ll2l(range.length); + return [cachedData subdataWithRange:cachedDataSubrange]; + } +} + +- (void)copyBytes:(unsigned char *)bytes range:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); // it doesn't make sense to ask for a buffer larger than can be stored in memory + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + [byteArray copyBytes:bytes range:range]; +} + +- (void)_updateDisplayedRange { + HFRange proposedNewDisplayRange; + HFFPRange proposedNewLineRange; + HFRange maxRangeSet = [self _maximumDisplayedRangeSet]; + NSUInteger maxBytesForViewSize = NSUIntegerMax; + double maxLines = DBL_MAX; + FOREACH(HFRepresenter*, rep, representers) { + NSView *view = [rep view]; + double repMaxLines = [rep maximumAvailableLinesForViewHeight:NSHeight([view frame])]; + if (repMaxLines != DBL_MAX) { + /* bytesPerLine may be ULONG_MAX. We want to compute the smaller of maxBytesForViewSize and ceil(repMaxLines) * bytesPerLine. If the latter expression overflows, the smaller is the former. */ + NSUInteger repMaxLinesUInt = (NSUInteger)ceil(repMaxLines); + NSUInteger maxLinesTimesBytesPerLine = repMaxLinesUInt * bytesPerLine; + /* Check if we overflowed */ + BOOL overflowed = (repMaxLinesUInt != 0 && (maxLinesTimesBytesPerLine / repMaxLinesUInt != bytesPerLine)); + if (! overflowed) { + maxBytesForViewSize = MIN(maxLinesTimesBytesPerLine, maxBytesForViewSize); + } + } + maxLines = MIN(repMaxLines, maxLines); + } + if (maxLines == DBL_MAX) { + proposedNewDisplayRange = HFRangeMake(0, 0); + proposedNewLineRange = (HFFPRange){0, 0}; + } + else { + unsigned long long maximumDisplayedBytes = MIN(maxRangeSet.length, maxBytesForViewSize); + HFASSERT(HFMaxRange(maxRangeSet) >= maximumDisplayedBytes); + + proposedNewDisplayRange.location = MIN(HFMaxRange(maxRangeSet) - maximumDisplayedBytes, displayedContentsRange.location); + proposedNewDisplayRange.location -= proposedNewDisplayRange.location % bytesPerLine; + proposedNewDisplayRange.length = MIN(HFMaxRange(maxRangeSet) - proposedNewDisplayRange.location, maxBytesForViewSize); + if (maxBytesForViewSize % bytesPerLine != 0) { + NSLog(@"Bad max bytes: %lu (%lu)", (unsigned long)maxBytesForViewSize, (unsigned long)bytesPerLine); + } + if (HFMaxRange(maxRangeSet) != ULLONG_MAX && (HFMaxRange(maxRangeSet) - proposedNewDisplayRange.location) % bytesPerLine != 0) { + NSLog(@"Bad max range minus: %llu (%lu)", HFMaxRange(maxRangeSet) - proposedNewDisplayRange.location, (unsigned long)bytesPerLine); + } + + long double lastLine = HFULToFP([self totalLineCount]); + proposedNewLineRange.length = MIN(maxLines, lastLine); + proposedNewLineRange.location = MIN(displayedLineRange.location, lastLine - proposedNewLineRange.length); + } + HFASSERT(HFRangeIsSubrangeOfRange(proposedNewDisplayRange, maxRangeSet)); + HFASSERT(proposedNewDisplayRange.location % bytesPerLine == 0); + if (! HFRangeEqualsRange(proposedNewDisplayRange, displayedContentsRange) || ! HFFPRangeEqualsRange(proposedNewLineRange, displayedLineRange)) { + displayedContentsRange = proposedNewDisplayRange; + displayedLineRange = proposedNewLineRange; + [self _addPropertyChangeBits:HFControllerDisplayedLineRange]; + } +} + +- (void)_ensureVisibilityOfLocation:(unsigned long long)location { + HFASSERT(location <= [self contentsLength]); + unsigned long long lineInt = location / bytesPerLine; + long double line = HFULToFP(lineInt); + HFASSERT(line >= 0); + line = MIN(line, HFULToFP([self totalLineCount]) - 1); + HFFPRange lineRange = [self displayedLineRange]; + HFFPRange newLineRange = lineRange; + if (line < lineRange.location) { + newLineRange.location = line; + } + else if (line >= lineRange.location + lineRange.length) { + HFASSERT(lineRange.location + lineRange.length >= 1); + newLineRange.location = lineRange.location + (line - (lineRange.location + lineRange.length - 1)); + } + [self setDisplayedLineRange:newLineRange]; +} + +- (void)maximizeVisibilityOfContentsRange:(HFRange)range { + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + + // Find the minimum move necessary to make range visible + HFFPRange displayRange = [self displayedLineRange]; + HFFPRange newDisplayRange = displayRange; + unsigned long long startLine = range.location / bytesPerLine; + unsigned long long endLine = HFDivideULLRoundingUp(HFRoundUpToNextMultipleSaturate(HFMaxRange(range), bytesPerLine), bytesPerLine); + HFASSERT(endLine > startLine || endLine == ULLONG_MAX); + long double linesInRange = HFULToFP(endLine - startLine); + long double linesToDisplay = MIN(displayRange.length, linesInRange); + HFASSERT(linesToDisplay <= linesInRange); + long double linesToMoveDownToMakeLastLineVisible = HFULToFP(endLine) - (displayRange.location + displayRange.length); + long double linesToMoveUpToMakeFirstLineVisible = displayRange.location - HFULToFP(startLine); + //HFASSERT(linesToMoveUpToMakeFirstLineVisible <= 0 || linesToMoveDownToMakeLastLineVisible <= 0); + // in general, we expect either linesToMoveUpToMakeFirstLineVisible to be <= zero, or linesToMoveDownToMakeLastLineVisible to be <= zero. However, if the available space is smaller than one line, then that won't be true. + if (linesToMoveDownToMakeLastLineVisible > 0) { + newDisplayRange.location += linesToMoveDownToMakeLastLineVisible; + } + else if (linesToMoveUpToMakeFirstLineVisible > 0 && linesToDisplay >= 1) { + // the >= 1 check prevents some wacky behavior when we have less than one line's worth of space, that caused bouncing between the top and bottom of the line + newDisplayRange.location -= linesToMoveUpToMakeFirstLineVisible; + } + + // Use the minimum movement if it would be visually helpful; otherwise just center. + if (HFFPIntersectsRange(displayRange, newDisplayRange)) { + [self setDisplayedLineRange:newDisplayRange]; + } else { + [self centerContentsRange:range]; + } +} + +- (void)centerContentsRange:(HFRange)range { + HFASSERT(HFRangeIsSubrangeOfRange(range, HFRangeMake(0, [self contentsLength]))); + HFFPRange displayRange = [self displayedLineRange]; + const long double numDisplayedLines = displayRange.length; + HFFPRange newDisplayRange; + unsigned long long startLine = range.location / bytesPerLine; + unsigned long long endLine = HFDivideULLRoundingUp(HFRoundUpToNextMultipleSaturate(HFMaxRange(range), bytesPerLine), bytesPerLine); + HFASSERT(endLine > startLine || endLine == ULLONG_MAX); + long double linesInRange = HFULToFP(endLine - startLine); + + /* Handle the case of a line range bigger than we can display by choosing the top lines. */ + if (numDisplayedLines <= linesInRange) { + newDisplayRange = (HFFPRange){startLine, numDisplayedLines}; + } + else { + /* Construct a newDisplayRange that centers {startLine, endLine} */ + long double center = startLine + (endLine - startLine) / 2.; + newDisplayRange = (HFFPRange){center - numDisplayedLines / 2., numDisplayedLines}; + } + + /* Move the newDisplayRange up or down as necessary */ + newDisplayRange.location = fmaxl(newDisplayRange.location, (long double)0.); + newDisplayRange.location = fminl(newDisplayRange.location, HFULToFP([self totalLineCount]) - numDisplayedLines); + [self setDisplayedLineRange:newDisplayRange]; +} + +/* Clips the selection to a given length. If this would clip the entire selection, returns a zero length selection at the end. Indicates HFControllerSelectedRanges if the selection changes. */ +- (void)_clipSelectedContentsRangesToLength:(unsigned long long)newLength { + NSMutableArray *newTempSelection = [selectedContentsRanges mutableCopy]; + NSUInteger i, max = [newTempSelection count]; + for (i=0; i < max; i++) { + HFRange range = [newTempSelection[i] HFRange]; + if (HFMaxRange(range) > newLength) { + if (range.location > newLength) { + /* The range starts past our new max. Just remove this range entirely */ + [newTempSelection removeObjectAtIndex:i]; + i--; + max--; + } + else { + /* Need to clip this range */ + range.length = newLength - range.location; + newTempSelection[i] = [HFRangeWrapper withRange:range]; + } + } + } + [newTempSelection setArray:[HFRangeWrapper organizeAndMergeRanges:newTempSelection]]; + + /* If there are multiple empty ranges, remove all but the first */ + BOOL foundEmptyRange = NO; + max = [newTempSelection count]; + for (i=0; i < max; i++) { + HFRange range = [newTempSelection[i] HFRange]; + HFASSERT(HFMaxRange(range) <= newLength); + if (range.length == 0) { + if (foundEmptyRange) { + [newTempSelection removeObjectAtIndex:i]; + i--; + max--; + } + foundEmptyRange = YES; + } + } + if (max == 0) { + /* Removed all ranges - insert one at the end */ + [newTempSelection addObject:[HFRangeWrapper withRange:HFRangeMake(newLength, 0)]]; + } + + /* If something changed, set the new selection and post the change bit */ + if (! [selectedContentsRanges isEqualToArray:newTempSelection]) { + [selectedContentsRanges setArray:newTempSelection]; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + + [newTempSelection release]; +} + +- (void)setByteArray:(HFByteArray *)val { + REQUIRE_NOT_NULL(val); + BEGIN_TRANSACTION(); + [byteArray removeObserver:self forKeyPath:@"changesAreLocked"]; + [val retain]; + [byteArray release]; + byteArray = val; + [cachedData release]; + cachedData = nil; + [byteArray addObserver:self forKeyPath:@"changesAreLocked" options:0 context:KVOContextChangesAreLocked]; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits: HFControllerContentValue | HFControllerContentLength]; + [self _clipSelectedContentsRangesToLength:[byteArray length]]; + END_TRANSACTION(); +} + +- (HFByteArray *)byteArray { + return byteArray; +} + +- (void)_undoNotification:note { + USE(note); +} + +- (void)_removeUndoManagerNotifications { + if (undoManager) { + NSNotificationCenter *noter = [NSNotificationCenter defaultCenter]; + [noter removeObserver:self name:NSUndoManagerWillUndoChangeNotification object:undoManager]; + } +} + +- (void)_addUndoManagerNotifications { + if (undoManager) { + NSNotificationCenter *noter = [NSNotificationCenter defaultCenter]; + [noter addObserver:self selector:@selector(_undoNotification:) name:NSUndoManagerWillUndoChangeNotification object:undoManager]; + } +} + +- (void)_removeAllUndoOperations { + /* Remove all the undo operations, because some undo operation is unsupported. Note that if we were smarter we would keep a stack of undo operations and only remove ones "up to" a certain point. */ + [undoManager removeAllActionsWithTarget:self]; + [undoOperations makeObjectsPerformSelector:@selector(invalidate)]; + [undoOperations removeAllObjects]; +} + +- (void)setUndoManager:(NSUndoManager *)manager { + [self _removeUndoManagerNotifications]; + [self _removeAllUndoOperations]; + [manager retain]; + [undoManager release]; + undoManager = manager; + [self _addUndoManagerNotifications]; +} + +- (NSUndoManager *)undoManager { + return undoManager; +} + +- (NSUInteger)bytesPerLine { + return bytesPerLine; +} + +- (BOOL)editable { + return _hfflags.editable && ! [byteArray changesAreLocked] && _hfflags.editMode != HFReadOnlyMode; +} + +- (void)setEditable:(BOOL)flag { + if (flag != _hfflags.editable) { + _hfflags.editable = flag; + [self _addPropertyChangeBits:HFControllerEditable]; + } +} + +- (void)_updateBytesPerLine { + NSUInteger newBytesPerLine = NSUIntegerMax; + FOREACH(HFRepresenter*, rep, representers) { + NSView *view = [rep view]; + CGFloat width = [view frame].size.width; + NSUInteger repMaxBytesPerLine = [rep maximumBytesPerLineForViewWidth:width]; + HFASSERT(repMaxBytesPerLine > 0); + newBytesPerLine = MIN(repMaxBytesPerLine, newBytesPerLine); + } + if (newBytesPerLine != bytesPerLine) { + HFASSERT(newBytesPerLine > 0); + bytesPerLine = newBytesPerLine; + BEGIN_TRANSACTION(); + [self _addPropertyChangeBits:HFControllerBytesPerLine]; + END_TRANSACTION(); + } +} + +- (void)representer:(HFRepresenter *)rep changedProperties:(HFControllerPropertyBits)properties { + USE(rep); + HFControllerPropertyBits remainingProperties = properties; + BEGIN_TRANSACTION(); + if (remainingProperties & HFControllerBytesPerLine) { + [self _updateBytesPerLine]; + remainingProperties &= ~HFControllerBytesPerLine; + } + if (remainingProperties & HFControllerDisplayedLineRange) { + [self _updateDisplayedRange]; + remainingProperties &= ~HFControllerDisplayedLineRange; + } + if (remainingProperties & HFControllerByteRangeAttributes) { + [self _addPropertyChangeBits:HFControllerByteRangeAttributes]; + remainingProperties &= ~HFControllerByteRangeAttributes; + } + if (remainingProperties & HFControllerViewSizeRatios) { + [self _addPropertyChangeBits:HFControllerViewSizeRatios]; + remainingProperties &= ~HFControllerViewSizeRatios; + } + if (remainingProperties) { + NSLog(@"Unknown properties: %lx", (long)remainingProperties); + } + END_TRANSACTION(); +} + +- (HFByteArray *)byteArrayForSelectedContentsRanges { + HFByteArray *result = nil; + HFByteArray *bytes = [self byteArray]; + VALIDATE_SELECTION(); + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + HFByteArray *additionalBytes = [bytes subarrayWithRange:range]; + if (! result) { + result = additionalBytes; + } + else { + [result insertByteArray:additionalBytes inRange:HFRangeMake([result length], 0)]; + } + } + return result; +} + +/* Flattens the selected range to a single range (the selected range becomes any character within or between the selected ranges). Modifies the selectedContentsRanges and returns the new single HFRange. Does not call notifyRepresentersOfChanges: */ +- (HFRange)_flattenSelectionRange { + HFASSERT([selectedContentsRanges count] >= 1); + + HFRange resultRange = [selectedContentsRanges[0] HFRange]; + if ([selectedContentsRanges count] == 1) return resultRange; //already flat + + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange selectedRange = [wrapper HFRange]; + if (selectedRange.location < resultRange.location) { + /* Extend our result range backwards */ + resultRange.length += resultRange.location - selectedRange.location; + resultRange.location = selectedRange.location; + } + if (HFRangeExtendsPastRange(selectedRange, resultRange)) { + HFASSERT(selectedRange.location >= resultRange.location); //must be true by if statement above + resultRange.length = HFSum(selectedRange.location - resultRange.location, selectedRange.length); + } + } + [self _setSingleSelectedContentsRange:resultRange]; + return resultRange; +} + +- (unsigned long long)_minimumSelectionLocation { + HFASSERT([selectedContentsRanges count] >= 1); + unsigned long long minSelection = ULLONG_MAX; + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + minSelection = MIN(minSelection, range.location); + } + return minSelection; +} + +- (unsigned long long)_maximumSelectionLocation { + HFASSERT([selectedContentsRanges count] >= 1); + unsigned long long maxSelection = 0; + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + maxSelection = MAX(maxSelection, HFMaxRange(range)); + } + return maxSelection; +} + +- (unsigned long long)minimumSelectionLocation { + return [self _minimumSelectionLocation]; +} + +- (unsigned long long)maximumSelectionLocation { + return [self _maximumSelectionLocation]; +} + +/* Put the selection at the left or right end of the current selection, with zero length. Modifies the selectedContentsRanges and returns the new single HFRange. Does not call notifyRepresentersOfChanges: */ +- (HFRange)_telescopeSelectionRangeInDirection:(HFControllerMovementDirection)direction { + HFRange resultRange; + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + resultRange.location = (direction == HFControllerDirectionLeft ? [self _minimumSelectionLocation] : [self _maximumSelectionLocation]); + resultRange.length = 0; + [self _setSingleSelectedContentsRange:resultRange]; + return resultRange; +} + +- (void)beginSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)characterIndex { + USE(event); + HFASSERT(characterIndex <= [self contentsLength]); + + /* Determine how to perform the selection - normally, with command key, or with shift key. Command + shift is the same as command. The shift key closes the selection - the selected range becomes the single range containing the first and last selected character. */ + _hfflags.shiftExtendSelection = NO; + _hfflags.commandExtendSelection = NO; + NSUInteger flags = [event modifierFlags]; + if (flags & NSCommandKeyMask) _hfflags.commandExtendSelection = YES; + else if (flags & NSShiftKeyMask) _hfflags.shiftExtendSelection = YES; + + selectionAnchor = NO_SELECTION; + selectionAnchorRange = HFRangeMake(NO_SELECTION, 0); + + _hfflags.selectionInProgress = YES; + if (_hfflags.commandExtendSelection) { + /* The selection anchor is used to track the "invert" range. All characters within this range have their selection inverted. This is tracked by the _shouldInvertSelectedRangesByAnchorRange method. */ + selectionAnchor = characterIndex; + selectionAnchorRange = HFRangeMake(characterIndex, 0); + } + else if (_hfflags.shiftExtendSelection) { + /* The selection anchor is used to track the single (flattened) selected range. */ + HFRange selectedRange = [self _flattenSelectionRange]; + unsigned long long distanceFromRangeStart = HFAbsoluteDifference(selectedRange.location, characterIndex); + unsigned long long distanceFromRangeEnd = HFAbsoluteDifference(HFMaxRange(selectedRange), characterIndex); + if (selectedRange.length == 0) { + HFASSERT(distanceFromRangeStart == distanceFromRangeEnd); + selectionAnchor = selectedRange.location; + selectedRange.location = MIN(characterIndex, selectedRange.location); + selectedRange.length = distanceFromRangeStart; + } + else if (distanceFromRangeStart >= distanceFromRangeEnd) { + /* Push the "end forwards" */ + selectedRange.length = distanceFromRangeStart; + selectionAnchor = selectedRange.location; + } + else { + /* Push the "start back" */ + selectedRange.location = selectedRange.location + selectedRange.length - distanceFromRangeEnd; + selectedRange.length = distanceFromRangeEnd; + selectionAnchor = HFSum(selectedRange.length, selectedRange.location); + } + HFASSERT(HFRangeIsSubrangeOfRange(selectedRange, HFRangeMake(0, [self contentsLength]))); + selectionAnchorRange = selectedRange; + [self _setSingleSelectedContentsRange:selectedRange]; + } + else { + /* No modifier key selection. The selection anchor is not used. */ + [self _setSingleSelectedContentsRange:HFRangeMake(characterIndex, 0)]; + selectionAnchor = characterIndex; + } +} + +- (void)continueSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)byteIndex { + USE(event); + HFASSERT(_hfflags.selectionInProgress); + HFASSERT(byteIndex <= [self contentsLength]); + BEGIN_TRANSACTION(); + if (_hfflags.commandExtendSelection) { + /* Clear any zero-length ranges, unless there's only one */ + NSUInteger rangeCount = [selectedContentsRanges count]; + NSUInteger rangeIndex = rangeCount; + while (rangeIndex-- > 0) { + if (rangeCount > 1 && [selectedContentsRanges[rangeIndex] HFRange].length == 0) { + [selectedContentsRanges removeObjectAtIndex:rangeIndex]; + rangeCount--; + } + } + selectionAnchorRange.location = MIN(byteIndex, selectionAnchor); + selectionAnchorRange.length = MAX(byteIndex, selectionAnchor) - selectionAnchorRange.location; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + else if (_hfflags.shiftExtendSelection) { + HFASSERT(selectionAnchorRange.location != NO_SELECTION); + HFASSERT(selectionAnchor != NO_SELECTION); + HFRange range; + if (! HFLocationInRange(byteIndex, selectionAnchorRange)) { + /* The character index is outside of the selection anchor range. The new range is just the selected anchor range combined with the character index. */ + range.location = MIN(byteIndex, selectionAnchorRange.location); + unsigned long long rangeEnd = MAX(byteIndex, HFSum(selectionAnchorRange.location, selectionAnchorRange.length)); + HFASSERT(rangeEnd >= range.location); + range.length = rangeEnd - range.location; + } + else { + /* The character is within the selection anchor range. We use the selection anchor index to determine which "side" of the range is selected. */ + range.location = MIN(selectionAnchor, byteIndex); + range.length = HFAbsoluteDifference(selectionAnchor, byteIndex); + } + [self _setSingleSelectedContentsRange:range]; + } + else { + /* No modifier key selection */ + HFRange range; + range.location = MIN(byteIndex, selectionAnchor); + range.length = MAX(byteIndex, selectionAnchor) - range.location; + [self _setSingleSelectedContentsRange:range]; + } + END_TRANSACTION(); + VALIDATE_SELECTION(); +} + +- (void)endSelectionWithEvent:(NSEvent *)event forByteIndex:(unsigned long long)characterIndex { + USE(event); + HFASSERT(_hfflags.selectionInProgress); + HFASSERT(characterIndex <= [self contentsLength]); + if (_hfflags.commandExtendSelection) { + selectionAnchorRange.location = MIN(characterIndex, selectionAnchor); + selectionAnchorRange.length = MAX(characterIndex, selectionAnchor) - selectionAnchorRange.location; + + /* "Commit" our selectionAnchorRange */ + NSArray *newSelection = [self _invertedSelectedContentsRanges]; + [selectedContentsRanges setArray:newSelection]; + } + else if (_hfflags.shiftExtendSelection) { + HFASSERT(selectionAnchorRange.location != NO_SELECTION); + HFASSERT(selectionAnchor != NO_SELECTION); + HFRange range; + if (! HFLocationInRange(characterIndex, selectionAnchorRange)) { + /* The character index is outside of the selection anchor range. The new range is just the selected anchor range combined with the character index. */ + range.location = MIN(characterIndex, selectionAnchorRange.location); + unsigned long long rangeEnd = MAX(characterIndex, HFSum(selectionAnchorRange.location, selectionAnchorRange.length)); + HFASSERT(rangeEnd >= range.location); + range.length = rangeEnd - range.location; + } + else { + /* The character is within the selection anchor range. We use the selection anchor index to determine which "side" of the range is selected. */ + range.location = MIN(selectionAnchor, characterIndex); + range.length = HFAbsoluteDifference(selectionAnchor, characterIndex); + } + [self _setSingleSelectedContentsRange:range]; + } + else { + /* No modifier key selection */ + HFRange range; + range.location = MIN(characterIndex, selectionAnchor); + range.length = MAX(characterIndex, selectionAnchor) - range.location; + [self _setSingleSelectedContentsRange:range]; + } + + _hfflags.selectionInProgress = NO; + _hfflags.shiftExtendSelection = NO; + _hfflags.commandExtendSelection = NO; + selectionAnchor = NO_SELECTION; +} + +- (double)selectionPulseAmount { + double result = 0; + if (pulseSelectionStartTime > 0) { + CFTimeInterval diff = pulseSelectionCurrentTime - pulseSelectionStartTime; + if (diff > 0 && diff < kPulseDuration) { + result = 1. - fabs(diff * 2 - kPulseDuration) / kPulseDuration; + } + } + return result; +} + +- (void)firePulseTimer:(NSTimer *)timer { + USE(timer); + HFASSERT(pulseSelectionStartTime != 0); + pulseSelectionCurrentTime = CFAbsoluteTimeGetCurrent(); + [self _addPropertyChangeBits:HFControllerSelectionPulseAmount]; + if (pulseSelectionCurrentTime - pulseSelectionStartTime > kPulseDuration) { + [pulseSelectionTimer invalidate]; + [pulseSelectionTimer release]; + pulseSelectionTimer = nil; + } +} + +- (void)pulseSelection { + pulseSelectionStartTime = CFAbsoluteTimeGetCurrent(); + if (pulseSelectionTimer == nil) { + pulseSelectionTimer = [[NSTimer scheduledTimerWithTimeInterval:(1. / 30.) target:self selector:@selector(firePulseTimer:) userInfo:nil repeats:YES] retain]; + } +} + +- (void)scrollByLines:(long double)lines { + HFFPRange lineRange = [self displayedLineRange]; + HFASSERT(HFULToFP([self totalLineCount]) >= lineRange.length); + long double maxScroll = HFULToFP([self totalLineCount]) - lineRange.length; + if (lines < 0) { + lineRange.location -= MIN(lineRange.location, -lines); + } + else { + lineRange.location = MIN(maxScroll, lineRange.location + lines); + } + [self setDisplayedLineRange:lineRange]; +} + +- (void)scrollWithScrollEvent:(NSEvent *)scrollEvent { + HFASSERT(scrollEvent != NULL); + HFASSERT([scrollEvent type] == NSScrollWheel); + CGFloat preciseScroll = 0; + BOOL hasPreciseScroll; + + /* Prefer precise deltas */ + if ([scrollEvent respondsToSelector:@selector(hasPreciseScrollingDeltas)]) { + hasPreciseScroll = [scrollEvent hasPreciseScrollingDeltas]; + if (hasPreciseScroll) { + /* In this case, we're going to scroll by a certain number of points */ + preciseScroll = [scrollEvent scrollingDeltaY]; + } + } else if ([scrollEvent respondsToSelector:@selector(deviceDeltaY)]) { + /* Legacy (SnowLeopard) support */ + hasPreciseScroll = ([scrollEvent subtype] == 1); + if (hasPreciseScroll) { + preciseScroll = [scrollEvent deviceDeltaY]; + } + } else { + hasPreciseScroll = NO; + } + + long double scrollY = 0; + if (! hasPreciseScroll) { + scrollY = -kScrollMultiplier * [scrollEvent deltaY]; + } else { + scrollY = -preciseScroll / [self lineHeight]; + } + [self scrollByLines:scrollY]; +} + +- (void)setSelectedContentsRanges:(NSArray *)selectedRanges { + REQUIRE_NOT_NULL(selectedRanges); + [selectedContentsRanges setArray:selectedRanges]; + VALIDATE_SELECTION(); + selectionAnchor = NO_SELECTION; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; +} + +- (IBAction)selectAll:sender { + USE(sender); + if (_hfflags.selectable) { + [self _setSingleSelectedContentsRange:HFRangeMake(0, [self contentsLength])]; + } +} + +- (void)_addRangeToSelection:(HFRange)range { + [selectedContentsRanges addObject:[HFRangeWrapper withRange:range]]; + [selectedContentsRanges setArray:[HFRangeWrapper organizeAndMergeRanges:selectedContentsRanges]]; + VALIDATE_SELECTION(); +} + +- (void)_removeRangeFromSelection:(HFRange)inputRange withCursorLocationIfAllSelectionRemoved:(unsigned long long)cursorLocation { + NSUInteger selectionCount = [selectedContentsRanges count]; + HFASSERT(selectionCount > 0 && selectionCount <= NSUIntegerMax / 2); + NSUInteger rangeIndex = 0; + NSArray *wrappers; + NEW_ARRAY(HFRange, tempRanges, selectionCount * 2); + FOREACH(HFRangeWrapper*, wrapper, selectedContentsRanges) { + HFRange range = [wrapper HFRange]; + if (! HFIntersectsRange(range, inputRange)) { + tempRanges[rangeIndex++] = range; + } + else { + if (range.location < inputRange.location) { + tempRanges[rangeIndex++] = HFRangeMake(range.location, inputRange.location - range.location); + } + if (HFMaxRange(range) > HFMaxRange(inputRange)) { + tempRanges[rangeIndex++] = HFRangeMake(HFMaxRange(inputRange), HFMaxRange(range) - HFMaxRange(inputRange)); + } + } + } + if (rangeIndex == 0 || (rangeIndex == 1 && tempRanges[0].length == 0)) { + /* We removed all of our ranges. Telescope us. */ + HFASSERT(cursorLocation <= [self contentsLength]); + [self _setSingleSelectedContentsRange:HFRangeMake(cursorLocation, 0)]; + } + else { + wrappers = [HFRangeWrapper withRanges:tempRanges count:rangeIndex]; + [selectedContentsRanges setArray:[HFRangeWrapper organizeAndMergeRanges:wrappers]]; + } + FREE_ARRAY(tempRanges); + VALIDATE_SELECTION(); +} + +- (void)_moveDirectionDiscardingSelection:(HFControllerMovementDirection)direction byAmount:(unsigned long long)amountToMove { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + BEGIN_TRANSACTION(); + BOOL selectionWasEmpty = ([selectedContentsRanges count] == 1 && [selectedContentsRanges[0] HFRange].length == 0); + BOOL directionIsForward = (direction == HFControllerDirectionRight); + HFRange selectedRange = [self _telescopeSelectionRangeInDirection: (directionIsForward ? HFControllerDirectionRight : HFControllerDirectionLeft)]; + HFASSERT(selectedRange.length == 0); + HFASSERT([self contentsLength] >= selectedRange.location); + /* A movement of just 1 with a selection only clears the selection; it does not move the cursor */ + if (selectionWasEmpty || amountToMove > 1) { + if (direction == HFControllerDirectionLeft) { + selectedRange.location -= MIN(amountToMove, selectedRange.location); + } + else { + selectedRange.location += MIN(amountToMove, [self contentsLength] - selectedRange.location); + } + } + selectionAnchor = NO_SELECTION; + [self _setSingleSelectedContentsRange:selectedRange]; + [self _ensureVisibilityOfLocation:selectedRange.location]; + END_TRANSACTION(); +} + +/* In _extendSelectionInDirection:byAmount:, we only allow left/right movement. up/down is not allowed. */ +- (void)_extendSelectionInDirection:(HFControllerMovementDirection)direction byAmount:(unsigned long long)amountToMove { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + unsigned long long minSelection = [self _minimumSelectionLocation]; + unsigned long long maxSelection = [self _maximumSelectionLocation]; + BOOL selectionChanged = NO; + unsigned long long locationToMakeVisible = NO_SELECTION; + unsigned long long contentsLength = [self contentsLength]; + if (selectionAnchor == NO_SELECTION) { + /* Pick the anchor opposite the choice of direction */ + if (direction == HFControllerDirectionLeft) selectionAnchor = maxSelection; + else selectionAnchor = minSelection; + } + if (direction == HFControllerDirectionLeft) { + if (minSelection >= selectionAnchor && maxSelection > minSelection) { + unsigned long long amountToRemove = llmin(maxSelection - selectionAnchor, amountToMove); + unsigned long long amountToAdd = llmin(amountToMove - amountToRemove, selectionAnchor); + if (amountToRemove > 0) [self _removeRangeFromSelection:HFRangeMake(maxSelection - amountToRemove, amountToRemove) withCursorLocationIfAllSelectionRemoved:minSelection]; + if (amountToAdd > 0) [self _addRangeToSelection:HFRangeMake(selectionAnchor - amountToAdd, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = (amountToAdd > 0 ? selectionAnchor - amountToAdd : maxSelection - amountToRemove); + } + else { + if (minSelection > 0) { + NSUInteger amountToAdd = ll2l(llmin(minSelection, amountToMove)); + if (amountToAdd > 0) [self _addRangeToSelection:HFRangeMake(minSelection - amountToAdd, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = minSelection - amountToAdd; + } + } + } + else if (direction == HFControllerDirectionRight) { + if (maxSelection <= selectionAnchor && maxSelection > minSelection) { + HFASSERT(contentsLength >= maxSelection); + unsigned long long amountToRemove = ll2l(llmin(maxSelection - minSelection, amountToMove)); + unsigned long long amountToAdd = amountToMove - amountToRemove; + if (amountToRemove > 0) [self _removeRangeFromSelection:HFRangeMake(minSelection, amountToRemove) withCursorLocationIfAllSelectionRemoved:maxSelection]; + if (amountToAdd > 0) [self _addRangeToSelection:HFRangeMake(maxSelection, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = llmin(contentsLength, (amountToAdd > 0 ? maxSelection + amountToAdd : minSelection + amountToRemove)); + } + else { + if (maxSelection < contentsLength) { + NSUInteger amountToAdd = ll2l(llmin(contentsLength - maxSelection, amountToMove)); + [self _addRangeToSelection:HFRangeMake(maxSelection, amountToAdd)]; + selectionChanged = YES; + locationToMakeVisible = maxSelection + amountToAdd; + } + } + } + if (selectionChanged) { + BEGIN_TRANSACTION(); + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + if (locationToMakeVisible != NO_SELECTION) [self _ensureVisibilityOfLocation:locationToMakeVisible]; + END_TRANSACTION(); + } +} + +/* Returns the distance to the next "word" (at least 1, unless we are empty). Here a word is identified as a column. If there are no columns, a word is a line. This is used for word movement (e.g. option + right arrow) */ +- (unsigned long long)_distanceToWordBoundaryForDirection:(HFControllerMovementDirection)direction { + unsigned long long result = 0, locationToConsider; + + /* Figure out how big a word is. By default, it's the column width, unless we have no columns, in which case it's the bytes per line. */ + NSUInteger wordGranularity = [self bytesPerColumn]; + if (wordGranularity == 0) wordGranularity = MAX(1u, [self bytesPerLine]); + if (selectionAnchor == NO_SELECTION) { + /* Pick the anchor inline with the choice of direction */ + if (direction == HFControllerDirectionLeft) locationToConsider = [self _minimumSelectionLocation]; + else locationToConsider = [self _maximumSelectionLocation]; + } else { + /* Just use the anchor */ + locationToConsider = selectionAnchor; + } + if (direction == HFControllerDirectionRight) { + result = HFRoundUpToNextMultipleSaturate(locationToConsider, wordGranularity) - locationToConsider; + } else { + result = locationToConsider % wordGranularity; + if (result == 0) result = wordGranularity; + } + return result; + +} + +/* Anchored selection is not allowed; neither is up/down movement */ +- (void)_shiftSelectionInDirection:(HFControllerMovementDirection)direction byAmount:(unsigned long long)amountToMove { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + HFASSERT(selectionAnchor == NO_SELECTION); + NSUInteger i, max = [selectedContentsRanges count]; + const unsigned long long maxLength = [self contentsLength]; + NSMutableArray *newRanges = [NSMutableArray arrayWithCapacity:max]; + BOOL hasAddedNonemptyRange = NO; + for (i=0; i < max; i++) { + HFRange range = [selectedContentsRanges[i] HFRange]; + HFASSERT(range.location <= maxLength && HFMaxRange(range) <= maxLength); + if (direction == HFControllerDirectionRight) { + unsigned long long offset = MIN(maxLength - range.location, amountToMove); + unsigned long long lengthToSubtract = MIN(range.length, amountToMove - offset); + range.location += offset; + range.length -= lengthToSubtract; + } + else { /* direction == HFControllerDirectionLeft */ + unsigned long long negOffset = MIN(amountToMove, range.location); + unsigned long long lengthToSubtract = MIN(range.length, amountToMove - negOffset); + range.location -= negOffset; + range.length -= lengthToSubtract; + } + [newRanges addObject:[HFRangeWrapper withRange:range]]; + hasAddedNonemptyRange = hasAddedNonemptyRange || (range.length > 0); + } + + newRanges = [[[HFRangeWrapper organizeAndMergeRanges:newRanges] mutableCopy] autorelease]; + + BOOL hasFoundEmptyRange = NO; + max = [newRanges count]; + for (i=0; i < max; i++) { + HFRange range = [newRanges[i] HFRange]; + if (range.length == 0) { + if (hasFoundEmptyRange || hasAddedNonemptyRange) { + [newRanges removeObjectAtIndex:i]; + i--; + max--; + } + hasFoundEmptyRange = YES; + } + } + [selectedContentsRanges setArray:newRanges]; + VALIDATE_SELECTION(); + [self _addPropertyChangeBits:HFControllerSelectedRanges]; +} + +__attribute__((unused)) +static BOOL rangesAreInAscendingOrder(NSEnumerator *rangeEnumerator) { + unsigned long long index = 0; + HFRangeWrapper *rangeWrapper; + while ((rangeWrapper = [rangeEnumerator nextObject])) { + HFRange range = [rangeWrapper HFRange]; + if (range.location < index) return NO; + index = HFSum(range.location, range.length); + } + return YES; +} + +- (BOOL)_registerCondemnedRangesForUndo:(NSArray *)ranges selectingRangesAfterUndo:(BOOL)selectAfterUndo { + HFASSERT(ranges != NULL); + HFASSERT(ranges != selectedContentsRanges); //selectedContentsRanges is mutable - we really don't want to stash it away with undo + BOOL result = NO; + NSUndoManager *manager = [self undoManager]; + NSUInteger rangeCount = [ranges count]; + if (! manager || ! rangeCount) return NO; + + HFASSERT(rangesAreInAscendingOrder([ranges objectEnumerator])); + + NSMutableArray *rangesToRestore = [NSMutableArray arrayWithCapacity:rangeCount]; + NSMutableArray *correspondingByteArrays = [NSMutableArray arrayWithCapacity:rangeCount]; + HFByteArray *bytes = [self byteArray]; + + /* Enumerate the ranges in forward order so when we insert them, we insert later ranges before earlier ones, so we don't have to worry about shifting indexes */ + FOREACH(HFRangeWrapper *, rangeWrapper, ranges) { + HFRange range = [rangeWrapper HFRange]; + if (range.length > 0) { + [rangesToRestore addObject:[HFRangeWrapper withRange:HFRangeMake(range.location, 0)]]; + [correspondingByteArrays addObject:[bytes subarrayWithRange:range]]; + result = YES; + } + } + + if (result) [self _registerUndoOperationForInsertingByteArrays:correspondingByteArrays inRanges:rangesToRestore withSelectionAction:(selectAfterUndo ? eSelectResult : eSelectAfterResult)]; + return result; +} + +- (void)_commandDeleteRanges:(NSArray *)rangesToDelete { + HFASSERT(rangesToDelete != selectedContentsRanges); //selectedContentsRanges is mutable - we really don't want to stash it away with undo + HFASSERT(rangesAreInAscendingOrder([rangesToDelete objectEnumerator])); + + /* Delete all the selection - in reverse order */ + unsigned long long minSelection = ULLONG_MAX; + BOOL somethingWasDeleted = NO; + [self _registerCondemnedRangesForUndo:rangesToDelete selectingRangesAfterUndo:YES]; + NSUInteger rangeIndex = [rangesToDelete count]; + HFASSERT(rangeIndex > 0); + while (rangeIndex--) { + HFRange range = [rangesToDelete[rangeIndex] HFRange]; + minSelection = llmin(range.location, minSelection); + if (range.length > 0) { + [byteArray deleteBytesInRange:range]; + somethingWasDeleted = YES; + } + } + + HFASSERT(minSelection != ULLONG_MAX); + if (somethingWasDeleted) { + BEGIN_TRANSACTION(); + [self _addPropertyChangeBits:HFControllerContentValue | HFControllerContentLength]; + [self _setSingleSelectedContentsRange:HFRangeMake(minSelection, 0)]; + [self _updateDisplayedRange]; + END_TRANSACTION(); + } + else { + NSBeep(); + } +} + +- (void)_commandInsertByteArrays:(NSArray *)byteArrays inRanges:(NSArray *)ranges withSelectionAction:(HFControllerSelectAction)selectionAction { + HFASSERT(selectionAction < NUM_SELECTION_ACTIONS); + REQUIRE_NOT_NULL(byteArrays); + REQUIRE_NOT_NULL(ranges); + HFASSERT([ranges count] == [byteArrays count]); + NSUInteger index, max = [ranges count]; + HFByteArray *bytes = [self byteArray]; + HFASSERT(rangesAreInAscendingOrder([ranges objectEnumerator])); + + NSMutableArray *byteArraysToInsertOnUndo = [NSMutableArray arrayWithCapacity:max]; + NSMutableArray *rangesToInsertOnUndo = [NSMutableArray arrayWithCapacity:max]; + + BEGIN_TRANSACTION(); + if (selectionAction == eSelectResult || selectionAction == eSelectAfterResult) { + [selectedContentsRanges removeAllObjects]; + } + unsigned long long endOfInsertedRanges = ULLONG_MAX; + for (index = 0; index < max; index++) { + HFRange range = [ranges[index] HFRange]; + HFByteArray *oldBytes = [bytes subarrayWithRange:range]; + [byteArraysToInsertOnUndo addObject:oldBytes]; + HFByteArray *newBytes = byteArrays[index]; + EXPECT_CLASS(newBytes, [HFByteArray class]); + [bytes insertByteArray:newBytes inRange:range]; + HFRange insertedRange = HFRangeMake(range.location, [newBytes length]); + HFRangeWrapper *insertedRangeWrapper = [HFRangeWrapper withRange:insertedRange]; + [rangesToInsertOnUndo addObject:insertedRangeWrapper]; + if (selectionAction == eSelectResult) { + [selectedContentsRanges addObject:insertedRangeWrapper]; + } + else { + endOfInsertedRanges = HFMaxRange(insertedRange); + } + } + if (selectionAction == eSelectAfterResult) { + HFASSERT([ranges count] > 0); + [selectedContentsRanges addObject:[HFRangeWrapper withRange:HFRangeMake(endOfInsertedRanges, 0)]]; + } + + if (selectionAction == ePreserveSelection) { + HFASSERT([selectedContentsRanges count] > 0); + [self _clipSelectedContentsRangesToLength:[self contentsLength]]; + } + + VALIDATE_SELECTION(); + HFASSERT([byteArraysToInsertOnUndo count] == [rangesToInsertOnUndo count]); + [self _registerUndoOperationForInsertingByteArrays:byteArraysToInsertOnUndo inRanges:rangesToInsertOnUndo withSelectionAction:(selectionAction == ePreserveSelection ? ePreserveSelection : eSelectAfterResult)]; + [self _updateDisplayedRange]; + [self maximizeVisibilityOfContentsRange:[selectedContentsRanges[0] HFRange]]; + [self _addPropertyChangeBits:HFControllerContentValue | HFControllerContentLength | HFControllerSelectedRanges]; + END_TRANSACTION(); +} + +/* The user has hit undo after typing a string. */ +- (void)_commandReplaceBytesAfterBytesFromBeginning:(unsigned long long)leftOffset upToBytesFromEnd:(unsigned long long)rightOffset withByteArray:(HFByteArray *)bytesToReinsert { + HFASSERT(bytesToReinsert != NULL); + + BEGIN_TRANSACTION(); + HFByteArray *bytes = [self byteArray]; + unsigned long long contentsLength = [self contentsLength]; + HFASSERT(leftOffset <= contentsLength); + HFASSERT(rightOffset <= contentsLength); + HFASSERT(contentsLength - rightOffset >= leftOffset); + HFRange rangeToReplace = HFRangeMake(leftOffset, contentsLength - rightOffset - leftOffset); + [self _registerCondemnedRangesForUndo:[HFRangeWrapper withRanges:&rangeToReplace count:1] selectingRangesAfterUndo:NO]; + [bytes insertByteArray:bytesToReinsert inRange:rangeToReplace]; + [self _updateDisplayedRange]; + [self _setSingleSelectedContentsRange:HFRangeMake(rangeToReplace.location, [bytesToReinsert length])]; + [self _addPropertyChangeBits:HFControllerContentValue | HFControllerContentLength | HFControllerSelectedRanges]; + END_TRANSACTION(); +} + +/* We use NSNumbers instead of long longs here because Tiger/PPC NSInvocation had trouble with long longs */ +- (void)_commandValueObjectsReplaceBytesAfterBytesFromBeginning:(NSNumber *)leftOffset upToBytesFromEnd:(NSNumber *)rightOffset withByteArray:(HFByteArray *)bytesToReinsert { + HFASSERT(leftOffset != NULL); + HFASSERT(rightOffset != NULL); + EXPECT_CLASS(leftOffset, NSNumber); + EXPECT_CLASS(rightOffset, NSNumber); + [self _commandReplaceBytesAfterBytesFromBeginning:[leftOffset unsignedLongLongValue] upToBytesFromEnd:[rightOffset unsignedLongLongValue] withByteArray:bytesToReinsert]; +} + +- (void)moveInDirection:(HFControllerMovementDirection)direction byByteCount:(unsigned long long)amountToMove withSelectionTransformation:(HFControllerSelectionTransformation)transformation usingAnchor:(BOOL)useAnchor { + if (! useAnchor) selectionAnchor = NO_SELECTION; + switch (transformation) { + case HFControllerDiscardSelection: + [self _moveDirectionDiscardingSelection:direction byAmount:amountToMove]; + break; + + case HFControllerShiftSelection: + [self _shiftSelectionInDirection:direction byAmount:amountToMove]; + break; + + case HFControllerExtendSelection: + [self _extendSelectionInDirection:direction byAmount:amountToMove]; + break; + + default: + [NSException raise:NSInvalidArgumentException format:@"Invalid transformation %ld", (long)transformation]; + break; + } + if (! useAnchor) selectionAnchor = NO_SELECTION; +} + +- (void)moveInDirection:(HFControllerMovementDirection)direction withGranularity:(HFControllerMovementGranularity)granularity andModifySelection:(BOOL)extendSelection { + HFASSERT(granularity == HFControllerMovementByte || granularity == HFControllerMovementColumn || granularity == HFControllerMovementLine || granularity == HFControllerMovementPage || granularity == HFControllerMovementDocument); + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + unsigned long long bytesToMove = 0; + switch (granularity) { + case HFControllerMovementByte: + bytesToMove = 1; + break; + case HFControllerMovementColumn: + /* This is a tricky case because the amount we have to move depends on our position in the column. */ + bytesToMove = [self _distanceToWordBoundaryForDirection:direction]; + break; + case HFControllerMovementLine: + bytesToMove = [self bytesPerLine]; + break; + case HFControllerMovementPage: + bytesToMove = HFProductULL([self bytesPerLine], HFFPToUL(MIN(floorl([self displayedLineRange].length), 1.))); + break; + case HFControllerMovementDocument: + bytesToMove = [self contentsLength]; + break; + } + HFControllerSelectionTransformation transformation = (extendSelection ? HFControllerExtendSelection : HFControllerDiscardSelection); + [self moveInDirection:direction byByteCount:bytesToMove withSelectionTransformation:transformation usingAnchor:YES]; +} + +- (void)moveToLineBoundaryInDirection:(HFControllerMovementDirection)direction andModifySelection:(BOOL)modifySelection { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + BEGIN_TRANSACTION(); + unsigned long long locationToMakeVisible; + HFRange additionalSelection; + + if (direction == HFControllerDirectionLeft) { + /* If we are at the beginning of a line, this should be a no-op */ + unsigned long long minLocation = [self _minimumSelectionLocation]; + unsigned long long newMinLocation = (minLocation / bytesPerLine) * bytesPerLine; + locationToMakeVisible = newMinLocation; + additionalSelection = HFRangeMake(newMinLocation, minLocation - newMinLocation); + } + else { + /* This always advances to the next line */ + unsigned long long maxLocation = [self _maximumSelectionLocation]; + unsigned long long proposedNewMaxLocation = HFRoundUpToNextMultipleSaturate(maxLocation, bytesPerLine); + unsigned long long newMaxLocation = MIN([self contentsLength], proposedNewMaxLocation); + HFASSERT(newMaxLocation >= maxLocation); + locationToMakeVisible = newMaxLocation; + additionalSelection = HFRangeMake(maxLocation, newMaxLocation - maxLocation); + } + + if (modifySelection) { + if (additionalSelection.length > 0) { + [self _addRangeToSelection:additionalSelection]; + [self _addPropertyChangeBits:HFControllerSelectedRanges]; + } + } + else { + [self _setSingleSelectedContentsRange:HFRangeMake(locationToMakeVisible, 0)]; + } + [self _ensureVisibilityOfLocation:locationToMakeVisible]; + END_TRANSACTION(); +} + +- (void)deleteSelection { + if ([self editMode] == HFOverwriteMode || ! [self editable]) { + NSBeep(); + } + else { + [self _commandDeleteRanges:[HFRangeWrapper organizeAndMergeRanges:selectedContentsRanges]]; + } +} + +// Called after Replace All is finished. +- (void)replaceByteArray:(HFByteArray *)newArray { + REQUIRE_NOT_NULL(newArray); + EXPECT_CLASS(newArray, HFByteArray); + HFRange entireRange = HFRangeMake(0, [self contentsLength]); + if ([self editMode] == HFOverwriteMode && [newArray length] != entireRange.length) { + NSBeep(); + } + else { + [self _commandInsertByteArrays:@[newArray] inRanges:[HFRangeWrapper withRanges:&entireRange count:1] withSelectionAction:ePreserveSelection]; + } +} + +- (BOOL)insertData:(NSData *)data replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing { + REQUIRE_NOT_NULL(data); + BOOL result; +#if ! NDEBUG + const unsigned long long startLength = [byteArray length]; + unsigned long long expectedNewLength; + if ([self editMode] == HFOverwriteMode) { + expectedNewLength = startLength; + } + else { + expectedNewLength = startLength + [data length] - previousBytes; + FOREACH(HFRangeWrapper*, wrapper, [self selectedContentsRanges]) expectedNewLength -= [wrapper HFRange].length; + } +#endif + HFByteSlice *slice = [[HFSharedMemoryByteSlice alloc] initWithUnsharedData:data]; + HFASSERT([slice length] == [data length]); + HFByteArray *array = [[preferredByteArrayClass() alloc] init]; + [array insertByteSlice:slice inRange:HFRangeMake(0, 0)]; + HFASSERT([array length] == [data length]); + result = [self insertByteArray:array replacingPreviousBytes:previousBytes allowUndoCoalescing:allowUndoCoalescing]; + [slice release]; + [array release]; +#if ! NDEBUG + HFASSERT((result && [byteArray length] == expectedNewLength) || (! result && [byteArray length] == startLength)); +#endif + return result; +} + +- (BOOL)_insertionModeCoreInsertByteArray:(HFByteArray *)bytesToInsert replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing outNewSingleSelectedRange:(HFRange *)outSelectedRange { + HFASSERT([self editMode] == HFInsertMode); + REQUIRE_NOT_NULL(bytesToInsert); + + /* Guard against overflow. If [bytesToInsert length] + [self contentsLength] - previousBytes overflows, then we can't do it */ + HFASSERT([self contentsLength] >= previousBytes); + if (! HFSumDoesNotOverflow([bytesToInsert length], [self contentsLength] - previousBytes)) { + return NO; //don't do anything + } + + + unsigned long long amountDeleted = 0, amountAdded = [bytesToInsert length]; + HFByteArray *bytes = [self byteArray]; + + /* Delete all the selection - in reverse order - except the last (really first) one, which we will overwrite. */ + NSArray *allRangesToRemove = [HFRangeWrapper organizeAndMergeRanges:[self selectedContentsRanges]]; + HFRange rangeToReplace = [allRangesToRemove[0] HFRange]; + HFASSERT(rangeToReplace.location == [self _minimumSelectionLocation]); + NSUInteger rangeIndex, rangeCount = [allRangesToRemove count]; + HFASSERT(rangeCount > 0); + NSMutableArray *rangesToDelete = [NSMutableArray arrayWithCapacity:rangeCount - 1]; + for (rangeIndex = rangeCount - 1; rangeIndex > 0; rangeIndex--) { + HFRangeWrapper *rangeWrapper = allRangesToRemove[rangeIndex]; + HFRange range = [rangeWrapper HFRange]; + if (range.length > 0) { + amountDeleted = HFSum(amountDeleted, range.length); + [rangesToDelete insertObject:rangeWrapper atIndex:0]; + } + } + + if ([rangesToDelete count] > 0) { + HFASSERT(rangesAreInAscendingOrder([rangesToDelete objectEnumerator])); + /* TODO: This is problematic because it overwrites the selection that gets set by _activateTypingUndoCoalescingForReplacingRange:, so we lose the first selection in a multiple selection scenario. */ + [self _registerCondemnedRangesForUndo:rangesToDelete selectingRangesAfterUndo:YES]; + NSEnumerator *enumer = [rangesToDelete reverseObjectEnumerator]; + HFRangeWrapper *rangeWrapper; + while ((rangeWrapper = [enumer nextObject])) { + [bytes deleteBytesInRange:[rangeWrapper HFRange]]; + } + } + + rangeToReplace.length = HFSum(rangeToReplace.length, previousBytes); + + /* Insert data */ +#if ! NDEBUG + unsigned long long expectedLength = [byteArray length] + [bytesToInsert length] - rangeToReplace.length; +#endif + [byteArray insertByteArray:bytesToInsert inRange:rangeToReplace]; +#if ! NDEBUG + HFASSERT(expectedLength == [byteArray length]); +#endif + + /* return the new selected range */ + *outSelectedRange = HFRangeMake(HFSum(rangeToReplace.location, amountAdded), 0); + return YES; +} + + +- (BOOL)_overwriteModeCoreInsertByteArray:(HFByteArray *)bytesToInsert replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing outRangeToRemoveFromSelection:(HFRange *)outRangeToRemove { + REQUIRE_NOT_NULL(bytesToInsert); + const unsigned long long byteArrayLength = [byteArray length]; + const unsigned long long bytesToInsertLength = [bytesToInsert length]; + HFRange firstSelectedRange = [selectedContentsRanges[0] HFRange]; + HFRange proposedRangeToOverwrite = HFRangeMake(firstSelectedRange.location, bytesToInsertLength); + HFASSERT(proposedRangeToOverwrite.location >= previousBytes); + proposedRangeToOverwrite.location -= previousBytes; + if (! HFRangeIsSubrangeOfRange(proposedRangeToOverwrite, HFRangeMake(0, byteArrayLength))) { + /* The user tried to overwrite past the end */ + NSBeep(); + return NO; + } + + [byteArray insertByteArray:bytesToInsert inRange:proposedRangeToOverwrite]; + + *outRangeToRemove = proposedRangeToOverwrite; + return YES; +} + +- (BOOL)insertByteArray:(HFByteArray *)bytesToInsert replacingPreviousBytes:(unsigned long long)previousBytes allowUndoCoalescing:(BOOL)allowUndoCoalescing { +#if ! NDEBUG + if (previousBytes > 0) { + NSArray *selectedRanges = [self selectedContentsRanges]; + HFASSERT([selectedRanges count] == 1); + HFRange selectedRange = [selectedRanges[0] HFRange]; + HFASSERT(selectedRange.location >= previousBytes); //don't try to delete more trailing bytes than we actually have! + } +#endif + REQUIRE_NOT_NULL(bytesToInsert); + + + BEGIN_TRANSACTION(); + unsigned long long beforeLength = [byteArray length]; + BOOL inOverwriteMode = [self editMode] == HFOverwriteMode; + HFRange modificationRange; //either range to remove from selection if in overwrite mode, or range to select if not + BOOL success; + if (inOverwriteMode) { + success = [self _overwriteModeCoreInsertByteArray:bytesToInsert replacingPreviousBytes:previousBytes allowUndoCoalescing:allowUndoCoalescing outRangeToRemoveFromSelection:&modificationRange]; + } + else { + success = [self _insertionModeCoreInsertByteArray:bytesToInsert replacingPreviousBytes:previousBytes allowUndoCoalescing:allowUndoCoalescing outNewSingleSelectedRange:&modificationRange]; + } + + if (success) { + /* Update our selection */ + [self _addPropertyChangeBits:HFControllerContentValue]; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits:HFControllerContentValue]; + if (inOverwriteMode) { + [self _removeRangeFromSelection:modificationRange withCursorLocationIfAllSelectionRemoved:HFMaxRange(modificationRange)]; + [self maximizeVisibilityOfContentsRange:[selectedContentsRanges[0] HFRange]]; + } + else { + [self _setSingleSelectedContentsRange:modificationRange]; + [self maximizeVisibilityOfContentsRange:modificationRange]; + } + if (beforeLength != [byteArray length]) [self _addPropertyChangeBits:HFControllerContentLength]; + } + END_TRANSACTION(); + return success; +} + +- (void)deleteDirection:(HFControllerMovementDirection)direction { + HFASSERT(direction == HFControllerDirectionLeft || direction == HFControllerDirectionRight); + if ([self editMode] != HFInsertMode || ! [self editable]) { + NSBeep(); + return; + } + unsigned long long minSelection = [self _minimumSelectionLocation]; + unsigned long long maxSelection = [self _maximumSelectionLocation]; + if (maxSelection != minSelection) { + [self deleteSelection]; + } + else { + HFRange rangeToDelete = HFRangeMake(minSelection, 1); + BOOL rangeIsValid; + if (direction == HFControllerDirectionLeft) { + rangeIsValid = (rangeToDelete.location > 0); + rangeToDelete.location--; + } + else { + rangeIsValid = (rangeToDelete.location < [self contentsLength]); + } + if (rangeIsValid) { + BEGIN_TRANSACTION(); + [byteArray deleteBytesInRange:rangeToDelete]; + [self _setSingleSelectedContentsRange:HFRangeMake(rangeToDelete.location, 0)]; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits:HFControllerSelectedRanges | HFControllerContentValue | HFControllerContentLength]; + END_TRANSACTION(); + } + } +} + +- (HFEditMode)editMode { + return _hfflags.editMode; +} + +- (void)setEditMode:(HFEditMode)val +{ + if (val != _hfflags.editMode) { + _hfflags.editMode = val; + // don't allow undo coalescing when switching modes + [self _addPropertyChangeBits:HFControllerEditable]; + } +} + +- (void)reloadData { + BEGIN_TRANSACTION(); + [cachedData release]; + cachedData = nil; + [self _updateDisplayedRange]; + [self _addPropertyChangeBits: HFControllerContentValue | HFControllerContentLength]; + END_TRANSACTION(); +} + +#if BENCHMARK_BYTEARRAYS + ++ (void)_testByteArray { + HFByteArray* first = [[[HFFullMemoryByteArray alloc] init] autorelease]; + HFBTreeByteArray* second = [[[HFBTreeByteArray alloc] init] autorelease]; + first = nil; + // second = nil; + + //srandom(time(NULL)); + + unsigned opCount = 4096 * 512; + unsigned long long expectedLength = 0; + unsigned i; + for (i=1; i <= opCount; i++) { + @autoreleasepool { + NSUInteger op; + const unsigned long long length = [first length]; + unsigned long long offset; + unsigned long long number; + switch ((op = (random()%2))) { + case 0: { //insert + offset = random() % (1 + length); + HFByteSlice* slice = [[HFRandomDataByteSlice alloc] initWithRandomDataLength: 1 + random() % 1000]; + [first insertByteSlice:slice inRange:HFRangeMake(offset, 0)]; + [second insertByteSlice:slice inRange:HFRangeMake(offset, 0)]; + expectedLength += [slice length]; + [slice release]; + break; + } + case 1: { //delete + if (length > 0) { + offset = random() % length; + number = 1 + random() % (length - offset); + [first deleteBytesInRange:HFRangeMake(offset, number)]; + [second deleteBytesInRange:HFRangeMake(offset, number)]; + expectedLength -= number; + } + break; + } + } + } // @autoreleasepool + } +} + ++ (void)_testAttributeArrays { + HFByteRangeAttributeArray *naiveTree = [[HFNaiveByteRangeAttributeArray alloc] init]; + HFAnnotatedTreeByteRangeAttributeArray *smartTree = [[HFAnnotatedTreeByteRangeAttributeArray alloc] init]; + naiveTree = nil; + // smartTree = nil; + + NSString * const attributes[3] = {@"Alpha", @"Beta", @"Gamma"}; + + const NSUInteger supportedIndexEnd = NSNotFound; + NSUInteger round; + for (round = 0; round < 4096 * 256; round++) { + NSString *attribute = attributes[random() % (sizeof attributes / sizeof *attributes)]; + BOOL insert = ([smartTree isEmpty] || [naiveTree isEmpty] || (random() % 2)); + + unsigned long long end = random(); + unsigned long long start = random(); + if (end < start) { + unsigned long long temp = end; + end = start; + start = temp; + } + HFRange range = HFRangeMake(start, end - start); + + if (insert) { + [naiveTree addAttribute:attribute range:range]; + [smartTree addAttribute:attribute range:range]; + } + else { + [naiveTree removeAttribute:attribute range:range]; + [smartTree removeAttribute:attribute range:range]; + } + } + + [naiveTree release]; + [smartTree release]; +} + + ++ (void)initialize { + CFAbsoluteTime start = CFAbsoluteTimeGetCurrent(); + srandom(0); + [self _testByteArray]; + CFAbsoluteTime end = CFAbsoluteTimeGetCurrent(); + printf("Byte array time: %f\n", end - start); + + srandom(0); + start = CFAbsoluteTimeGetCurrent(); + [self _testAttributeArrays]; + end = CFAbsoluteTimeGetCurrent(); + printf("Attribute array time: %f\n", end - start); + + exit(0); +} + +#endif + +@end diff --git a/bsnes/gb/HexFiend/HFFullMemoryByteArray.h b/bsnes/gb/HexFiend/HFFullMemoryByteArray.h new file mode 100644 index 00000000..9debeb2e --- /dev/null +++ b/bsnes/gb/HexFiend/HFFullMemoryByteArray.h @@ -0,0 +1,21 @@ +// +// HFFullMemoryByteArray.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! + @class HFFullMemoryByteArray + @brief A naive subclass of HFByteArray suitable mainly for testing. Use HFBTreeByteArray instead. + + HFFullMemoryByteArray is a simple subclass of HFByteArray that does not store any byte slices. Because it stores all data in an NSMutableData, it is not efficient. It is mainly useful as a naive implementation for testing. Use HFBTreeByteArray instead. +*/ +@interface HFFullMemoryByteArray : HFByteArray { + NSMutableData *data; +} + + +@end diff --git a/bsnes/gb/HexFiend/HFFullMemoryByteArray.m b/bsnes/gb/HexFiend/HFFullMemoryByteArray.m new file mode 100644 index 00000000..f5a582b9 --- /dev/null +++ b/bsnes/gb/HexFiend/HFFullMemoryByteArray.m @@ -0,0 +1,70 @@ +// +// HFFullMemoryByteArray.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import + +@implementation HFFullMemoryByteArray + +- (instancetype)init { + self = [super init]; + data = [[NSMutableData alloc] init]; + return self; +} + +- (void)dealloc { + [data release]; + [super dealloc]; +} + +- (unsigned long long)length { + return [data length]; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)range { + HFASSERT(range.length == 0 || dst != NULL); + HFASSERT(HFSumDoesNotOverflow(range.location, range.length)); + HFASSERT(range.location + range.length <= [self length]); + unsigned char* bytes = [data mutableBytes]; + memmove(dst, bytes + ll2l(range.location), ll2l(range.length)); +} + +- (HFByteArray *)subarrayWithRange:(HFRange)lrange { + HFRange entireRange = HFRangeMake(0, [self length]); + HFASSERT(HFRangeIsSubrangeOfRange(lrange, entireRange)); + NSRange range; + range.location = ll2l(lrange.location); + range.length = ll2l(lrange.length); + HFFullMemoryByteArray* result = [[[self class] alloc] init]; + [result->data setData:[data subdataWithRange:range]]; + return [result autorelease]; +} + +- (NSArray *)byteSlices { + return @[[[[HFFullMemoryByteSlice alloc] initWithData:data] autorelease]]; +} + +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange { + [self incrementGenerationOrRaiseIfLockedForSelector:_cmd]; + HFASSERT([slice length] <= NSUIntegerMax); + NSUInteger length = ll2l([slice length]); + NSRange range; + HFASSERT(lrange.location <= NSUIntegerMax); + HFASSERT(lrange.length <= NSUIntegerMax); + HFASSERT(HFSumDoesNotOverflow(lrange.location, lrange.length)); + range.location = ll2l(lrange.location); + range.length = ll2l(lrange.length); + + void* buff = check_malloc(length); + [slice copyBytes:buff range:HFRangeMake(0, length)]; + [data replaceBytesInRange:range withBytes:buff length:length]; + free(buff); +} + +@end diff --git a/bsnes/gb/HexFiend/HFFullMemoryByteSlice.h b/bsnes/gb/HexFiend/HFFullMemoryByteSlice.h new file mode 100644 index 00000000..ec195cad --- /dev/null +++ b/bsnes/gb/HexFiend/HFFullMemoryByteSlice.h @@ -0,0 +1,21 @@ +// +// HFFullMemoryByteSlice.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFFullMemoryByteSlice + + @brief A simple subclass of HFByteSlice that wraps an NSData. For most uses, prefer HFSharedMemoryByteSlice. +*/ +@interface HFFullMemoryByteSlice : HFByteSlice { + NSData *data; +} + +/*! Init with a given NSData, which is copied via the \c -copy message. */ +- (instancetype)initWithData:(NSData *)val; + +@end diff --git a/bsnes/gb/HexFiend/HFFullMemoryByteSlice.m b/bsnes/gb/HexFiend/HFFullMemoryByteSlice.m new file mode 100644 index 00000000..2a38ccdf --- /dev/null +++ b/bsnes/gb/HexFiend/HFFullMemoryByteSlice.m @@ -0,0 +1,46 @@ +// +// HFFullMemoryByteSlice.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import "HFFullMemoryByteSlice.h" + + +@implementation HFFullMemoryByteSlice + +- (instancetype)initWithData:(NSData *)val { + REQUIRE_NOT_NULL(val); + self = [super init]; + data = [val copy]; + return self; +} + +- (void)dealloc { + [data release]; + [super dealloc]; +} + +- (unsigned long long)length { return [data length]; } + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)lrange { + NSRange range; + HFASSERT(lrange.location <= NSUIntegerMax); + HFASSERT(lrange.length <= NSUIntegerMax); + HFASSERT(lrange.location + lrange.length >= lrange.location); + range.location = ll2l(lrange.location); + range.length = ll2l(lrange.length); + [data getBytes:dst range:range]; +} + +- (HFByteSlice *)subsliceWithRange:(HFRange)range { + HFASSERT(range.length > 0); + HFASSERT(range.location < [self length]); + HFASSERT([self length] - range.location >= range.length); + HFASSERT(range.location <= NSUIntegerMax); + HFASSERT(range.length <= NSUIntegerMax); + return [[[[self class] alloc] initWithData:[data subdataWithRange:NSMakeRange(ll2l(range.location), ll2l(range.length))]] autorelease]; +} + +@end diff --git a/bsnes/gb/HexFiend/HFFunctions.h b/bsnes/gb/HexFiend/HFFunctions.h new file mode 100644 index 00000000..aaab1076 --- /dev/null +++ b/bsnes/gb/HexFiend/HFFunctions.h @@ -0,0 +1,533 @@ +/* Functions and convenience methods for working with HFTypes */ + +#import +#import + +#define HFDEFAULT_FONT (@"Monaco") +#define HFDEFAULT_FONTSIZE ((CGFloat)11.) + +#define HFZeroRange (HFRange){0, 0} + +/*! + Makes an HFRange. An HFRange is like an NSRange except it uses unsigned long longs. +*/ +static inline HFRange HFRangeMake(unsigned long long loc, unsigned long long len) { + return (HFRange){loc, len}; +} + +/*! + Returns true if a given location is within a given HFRange. If the location is at the end of the range (range.location + range.length) this returns NO. +*/ +static inline BOOL HFLocationInRange(unsigned long long location, HFRange range) { + return location >= range.location && location - range.location < range.length; +} + +/*! + Like NSRangeToString but for HFRanges +*/ +static inline NSString* HFRangeToString(HFRange range) { + return [NSString stringWithFormat:@"{%llu, %llu}", range.location, range.length]; +} + +/*! + Converts a given HFFPRange to a string. +*/ +static inline NSString* HFFPRangeToString(HFFPRange range) { + return [NSString stringWithFormat:@"{%Lf, %Lf}", range.location, range.length]; +} + +/*! + Returns true if two HFRanges are equal. +*/ +static inline BOOL HFRangeEqualsRange(HFRange a, HFRange b) { + return a.location == b.location && a.length == b.length; +} + +/*! + Returns true if a + b does not overflow an unsigned long long. +*/ +static inline BOOL HFSumDoesNotOverflow(unsigned long long a, unsigned long long b) { + return a + b >= a; +} + +/*! + Returns true if a * b does not overflow an unsigned long long. +*/ +static inline BOOL HFProductDoesNotOverflow(unsigned long long a, unsigned long long b) { + if (b == 0) return YES; + unsigned long long result = a * b; + return result / b == a; +} + +/*! + Returns a * b as an NSUInteger. This asserts on overflow, unless NDEBUG is defined. +*/ +static inline NSUInteger HFProductInt(NSUInteger a, NSUInteger b) { + NSUInteger result = a * b; + assert(a == 0 || result / a == b); //detect overflow + return result; +} + +/*! + Returns a + b as an NSUInteger. This asserts on overflow unless NDEBUG is defined. +*/ +static inline NSUInteger HFSumInt(NSUInteger a, NSUInteger b) { + assert(a + b >= a); + return a + b; +} + +/*! + Returns a + b as an NSUInteger, saturating at NSUIntegerMax + */ +static inline NSUInteger HFSumIntSaturate(NSUInteger a, NSUInteger b) { + NSUInteger result = a + b; + return (result < a) ? NSUIntegerMax : result; +} + +/*! + Returns a + b as an unsigned long long, saturating at ULLONG_MAX + */ +static inline unsigned long long HFSumULLSaturate(unsigned long long a, unsigned long long b) { + unsigned long long result = a + b; + return (result < a) ? ULLONG_MAX : result; +} + +/*! + Returns a * b as an unsigned long long. This asserts on overflow, unless NDEBUG is defined. +*/ +static inline unsigned long long HFProductULL(unsigned long long a, unsigned long long b) { + unsigned long long result = a * b; + assert(HFProductDoesNotOverflow(a, b)); //detect overflow + return result; +} + +/*! + Returns a + b as an unsigned long long. This asserts on overflow, unless NDEBUG is defined. +*/ +static inline unsigned long long HFSum(unsigned long long a, unsigned long long b) { + assert(HFSumDoesNotOverflow(a, b)); + return a + b; +} + +/*! + Returns a + b as an unsigned long long. This asserts on overflow, unless NDEBUG is defined. + */ +static inline unsigned long long HFMaxULL(unsigned long long a, unsigned long long b) { + return a < b ? b : a; +} + +/*! + Returns a - b as an unsigned long long. This asserts on underflow (if b > a), unless NDEBUG is defined. +*/ +static inline unsigned long long HFSubtract(unsigned long long a, unsigned long long b) { + assert(a >= b); + return a - b; +} + +/*! + Returns the smallest multiple of B that is equal to or larger than A, and asserts on overflow. +*/ +static inline unsigned long long HFRoundUpToMultiple(unsigned long long a, unsigned long long b) { + // The usual approach of ((a + (b - 1)) / b) * b doesn't handle overflow correctly + unsigned long long remainder = a % b; + if (remainder == 0) return a; + else return HFSum(a, b - remainder); +} + +/*! + Returns the smallest multiple of B that is equal to or larger than A, and asserts on overflow. + */ +static inline NSUInteger HFRoundUpToMultipleInt(NSUInteger a, NSUInteger b) { + // The usual approach of ((a + (b - 1)) / b) * b doesn't handle overflow correctly + NSUInteger remainder = a % b; + if (remainder == 0) return a; + else return (NSUInteger)HFSum(a, b - remainder); +} + +/*! + Returns the least common multiple of A and B, and asserts on overflow or if A or B is zero. + */ +static inline NSUInteger HFLeastCommonMultiple(NSUInteger a, NSUInteger b) { + assert(a > 0); + assert(b > 0); + + /* Compute GCD. It ends up in U. */ + NSUInteger t, u = a, v = b; + while (v > 0) { + t = v; + v = u % v; + u = t; + } + + /* Return the product divided by the GCD, in an overflow safe manner */ + return HFProductInt(a/u, b); +} + + +/*! + Returns the smallest multiple of B strictly larger than A, or ULLONG_MAX if it would overflow +*/ +static inline unsigned long long HFRoundUpToNextMultipleSaturate(unsigned long long a, unsigned long long b) { + assert(b > 0); + unsigned long long result = a + (b - a % b); + if (result < a) result = ULLONG_MAX; //the saturation...on overflow go to the max + return result; +} + +/*! Like NSMaxRange, but for an HFRange. */ +static inline unsigned long long HFMaxRange(HFRange a) { + assert(HFSumDoesNotOverflow(a.location, a.length)); + return a.location + a.length; +} + +/*! Returns YES if needle is fully contained within haystack. Equal ranges are always considered to be subranges of each other (even if they are empty). Furthermore, a zero length needle at the end of haystack is considered a subrange - for example, {6, 0} is a subrange of {3, 3}. */ +static inline BOOL HFRangeIsSubrangeOfRange(HFRange needle, HFRange haystack) { + // If needle starts before haystack, or if needle is longer than haystack, it is not a subrange of haystack + if (needle.location < haystack.location || needle.length > haystack.length) return NO; + + // Their difference in lengths determines the maximum difference in their start locations. We know that these expressions cannot overflow because of the above checks. + return haystack.length - needle.length >= needle.location - haystack.location; +} + +/*! Splits a range about a subrange, returning by reference the prefix and suffix (which may have length zero). */ +static inline void HFRangeSplitAboutSubrange(HFRange range, HFRange subrange, HFRange *outPrefix, HFRange *outSuffix) { + // Requires it to be a subrange + assert(HFRangeIsSubrangeOfRange(subrange, range)); + outPrefix->location = range.location; + outPrefix->length = HFSubtract(subrange.location, range.location); + outSuffix->location = HFMaxRange(subrange); + outSuffix->length = HFMaxRange(range) - outSuffix->location; +} + +/*! Returns YES if the given ranges intersect. Two ranges are considered to intersect if they share at least one index in common. Thus, zero-length ranges do not intersect anything. */ +static inline BOOL HFIntersectsRange(HFRange a, HFRange b) { + // Ranges are said to intersect if they share at least one value. Therefore, zero length ranges never intersect anything. + if (a.length == 0 || b.length == 0) return NO; + + // rearrange (a.location < b.location + b.length && b.location < a.location + a.length) to not overflow + // = ! (a.location >= b.location + b.length || b.location >= a.location + a.length) + BOOL clause1 = (a.location >= b.location && a.location - b.location >= b.length); + BOOL clause2 = (b.location >= a.location && b.location - a.location >= a.length); + return ! (clause1 || clause2); +} + +/*! Returns YES if the given ranges intersect. Two ranges are considered to intersect if any fraction overlaps; zero-length ranges do not intersect anything. */ +static inline BOOL HFFPIntersectsRange(HFFPRange a, HFFPRange b) { + // Ranges are said to intersect if they share at least one value. Therefore, zero length ranges never intersect anything. + if (a.length == 0 || b.length == 0) return NO; + + if (a.location <= b.location && a.location + a.length >= b.location) return YES; + if (b.location <= a.location && b.location + b.length >= a.location) return YES; + return NO; +} + +/*! Returns a range containing the union of the given ranges. These ranges must either intersect or be adjacent: there cannot be any "holes" between them. */ +static inline HFRange HFUnionRange(HFRange a, HFRange b) { + assert(HFIntersectsRange(a, b) || HFMaxRange(a) == b.location || HFMaxRange(b) == a.location); + HFRange result; + result.location = MIN(a.location, b.location); + assert(HFSumDoesNotOverflow(a.location, a.length)); + assert(HFSumDoesNotOverflow(b.location, b.length)); + result.length = MAX(a.location + a.length, b.location + b.length) - result.location; + return result; +} + + +/*! Returns whether a+b > c+d, as if there were no overflow (so ULLONG_MAX + 1 > 10 + 20) */ +static inline BOOL HFSumIsLargerThanSum(unsigned long long a, unsigned long long b, unsigned long long c, unsigned long long d) { +#if 1 + // Theory: compare a/2 + b/2 to c/2 + d/2, and if they're equal, compare a%2 + b%2 to c%2 + d%2. We may get into trouble if a and b are both even and c and d are both odd: e.g. a = 2, b = 2, c = 1, d = 3. We would compare 1 + 1 vs 0 + 1, and therefore that 2 + 2 > 1 + 3. To address this, if both remainders are 1, we add this to the sum. We know this cannot overflow because ULLONG_MAX is odd, so (ULLONG_MAX/2) + (ULLONG_MAX/2) + 1 does not overflow. + unsigned int rem1 = (unsigned)(a%2 + b%2); + unsigned int rem2 = (unsigned)(c%2 + d%2); + unsigned long long sum1 = a/2 + b/2 + rem1/2; + unsigned long long sum2 = c/2 + d/2 + rem2/2; + if (sum1 > sum2) return YES; + else if (sum1 < sum2) return NO; + else { + // sum1 == sum2, so compare the remainders. But we have already added in the remainder / 2, so compare the remainders mod 2. + if (rem1%2 > rem2%2) return YES; + else return NO; + } +#else + /* Faster version, but not thoroughly tested yet. */ + unsigned long long xor1 = a^b; + unsigned long long xor2 = c^d; + unsigned long long avg1 = (a&b)+(xor1/2); + unsigned long long avg2 = (c&d)+(xor2/2); + unsigned s1l = avg1 > avg2; + unsigned eq = (avg1 == avg2); + return s1l | ((xor1 & ~xor2) & eq); +#endif +} + +/*! Returns the absolute value of a - b. */ +static inline unsigned long long HFAbsoluteDifference(unsigned long long a, unsigned long long b) { + if (a > b) return a - b; + else return b - a; +} + +/*! Returns true if the end of A is larger than the end of B. */ +static inline BOOL HFRangeExtendsPastRange(HFRange a, HFRange b) { + return HFSumIsLargerThanSum(a.location, a.length, b.location, b.length); +} + +/*! Returns a range containing all indexes in common betwen the two ranges. If there are no indexes in common, returns {0, 0}. */ +static inline HFRange HFIntersectionRange(HFRange range1, HFRange range2) { + unsigned long long minend = HFRangeExtendsPastRange(range2, range1) ? range1.location + range1.length : range2.location + range2.length; + if (range2.location <= range1.location && range1.location - range2.location < range2.length) { + return HFRangeMake(range1.location, minend - range1.location); + } + else if (range1.location <= range2.location && range2.location - range1.location < range1.length) { + return HFRangeMake(range2.location, minend - range2.location); + } + return HFRangeMake(0, 0); +} + +/*! ceil() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFCeil(CGFloat a) { + if (sizeof(a) == sizeof(float)) return (CGFloat)ceilf((float)a); + else return (CGFloat)ceil((double)a); +} + +/*! floor() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFFloor(CGFloat a) { + if (sizeof(a) == sizeof(float)) return (CGFloat)floorf((float)a); + else return (CGFloat)floor((double)a); +} + +/*! round() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFRound(CGFloat a) { + if (sizeof(a) == sizeof(float)) return (CGFloat)roundf((float)a); + else return (CGFloat)round((double)a); +} + +/*! fmin() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFMin(CGFloat a, CGFloat b) { + if (sizeof(a) == sizeof(float)) return (CGFloat)fminf((float)a, (float)b); + else return (CGFloat)fmin((double)a, (double)b); +} + +/*! fmax() for a CGFloat, for compatibility with OSes that do not have the CG versions. */ +static inline CGFloat HFMax(CGFloat a, CGFloat b) { + if (sizeof(a) == sizeof(float)) return (CGFloat)fmaxf((float)a, (float)b); + else return (CGFloat)fmax((double)a, (double)b); +} + +/*! Returns true if the given HFFPRanges are equal. */ +static inline BOOL HFFPRangeEqualsRange(HFFPRange a, HFFPRange b) { + return a.location == b.location && a.length == b.length; +} + +/*! copysign() for a CGFloat */ +static inline CGFloat HFCopysign(CGFloat a, CGFloat b) { +#if CGFLOAT_IS_DOUBLE + return copysign(a, b); +#else + return copysignf(a, b); +#endif +} + +/*! Atomically increments an NSUInteger, returning the new value. Optionally invokes a memory barrier. */ +static inline NSUInteger HFAtomicIncrement(volatile NSUInteger *ptr, BOOL barrier) { + return _Generic(ptr, + volatile unsigned *: (barrier ? OSAtomicIncrement32Barrier : OSAtomicIncrement32)((volatile int32_t *)ptr), +#if ULONG_MAX == UINT32_MAX + volatile unsigned long *: (barrier ? OSAtomicIncrement32Barrier : OSAtomicIncrement32)((volatile int32_t *)ptr), +#else + volatile unsigned long *: (barrier ? OSAtomicIncrement64Barrier : OSAtomicIncrement64)((volatile int64_t *)ptr), +#endif + volatile unsigned long long *: (barrier ? OSAtomicIncrement64Barrier : OSAtomicIncrement64)((volatile int64_t *)ptr)); +} + +/*! Atomically decrements an NSUInteger, returning the new value. Optionally invokes a memory barrier. */ +static inline NSUInteger HFAtomicDecrement(volatile NSUInteger *ptr, BOOL barrier) { + return _Generic(ptr, + volatile unsigned *: (barrier ? OSAtomicDecrement32Barrier : OSAtomicDecrement32)((volatile int32_t *)ptr), +#if ULONG_MAX == UINT32_MAX + volatile unsigned long *: (barrier ? OSAtomicDecrement32Barrier : OSAtomicDecrement32)((volatile int32_t *)ptr), +#else + volatile unsigned long *: (barrier ? OSAtomicDecrement64Barrier : OSAtomicDecrement64)((volatile int64_t *)ptr), +#endif + volatile unsigned long long *: (barrier ? OSAtomicDecrement64Barrier : OSAtomicDecrement64)((volatile int64_t *)ptr)); +} + +/*! Converts a long double to unsigned long long. Assumes that val is already an integer - use floorl or ceill */ +static inline unsigned long long HFFPToUL(long double val) { + assert(val >= 0); + assert(val <= ULLONG_MAX); + unsigned long long result = (unsigned long long)val; + assert((long double)result == val); + return result; +} + +/*! Converts an unsigned long long to a long double. */ +static inline long double HFULToFP(unsigned long long val) { + long double result = (long double)val; + assert(HFFPToUL(result) == val); + return result; +} + +/*! Convenience to return information about a CGAffineTransform for logging. */ +static inline NSString *HFDescribeAffineTransform(CGAffineTransform t) { + return [NSString stringWithFormat:@"%f %f 0\n%f %f 0\n%f %f 1", t.a, t.b, t.c, t.d, t.tx, t.ty]; +} + +/*! Returns 1 + floor(log base 10 of val). If val is 0, returns 1. */ +static inline NSUInteger HFCountDigitsBase10(unsigned long long val) { + const unsigned long long kValues[] = {0ULL, 9ULL, 99ULL, 999ULL, 9999ULL, 99999ULL, 999999ULL, 9999999ULL, 99999999ULL, 999999999ULL, 9999999999ULL, 99999999999ULL, 999999999999ULL, 9999999999999ULL, 99999999999999ULL, 999999999999999ULL, 9999999999999999ULL, 99999999999999999ULL, 999999999999999999ULL, 9999999999999999999ULL}; + NSUInteger low = 0, high = sizeof kValues / sizeof *kValues; + while (high > low) { + NSUInteger mid = (low + high)/2; //low + high cannot overflow + if (val > kValues[mid]) { + low = mid + 1; + } + else { + high = mid; + } + } + return MAX(1u, low); +} + +/*! Returns 1 + floor(log base 16 of val). If val is 0, returns 1. This works by computing the log base 2 based on the number of leading zeros, and then dividing by 4. */ +static inline NSUInteger HFCountDigitsBase16(unsigned long long val) { + /* __builtin_clzll doesn't like being passed 0 */ + if (val == 0) return 1; + + /* Compute the log base 2 */ + NSUInteger leadingZeros = (NSUInteger)__builtin_clzll(val); + NSUInteger logBase2 = (CHAR_BIT * sizeof val) - leadingZeros - 1; + return 1 + logBase2/4; +} + +/*! Returns YES if the given string encoding is a superset of ASCII. */ +BOOL HFStringEncodingIsSupersetOfASCII(NSStringEncoding encoding); + +/*! Returns the "granularity" of an encoding, in bytes. ASCII is 1, UTF-16 is 2, etc. Variable width encodings return the smallest (e.g. Shift-JIS returns 1). */ +uint8_t HFStringEncodingCharacterLength(NSStringEncoding encoding); + +/*! Converts an unsigned long long to NSUInteger. The unsigned long long should be no more than ULONG_MAX. */ +static inline NSUInteger ll2l(unsigned long long val) { assert(val <= ULONG_MAX); return (unsigned long)val; } + +/*! Converts an unsigned long long to uintptr_t. The unsigned long long should be no more than UINTPTR_MAX. */ +static inline uintptr_t ll2p(unsigned long long val) { assert(val <= UINTPTR_MAX); return (uintptr_t)val; } + +/*! Returns an unsigned long long, which must be no more than ULLONG_MAX, as an unsigned long. */ +static inline CGFloat ld2f(long double val) { +#if ! NDEBUG + if (isfinite(val)) { + assert(val <= CGFLOAT_MAX); + assert(val >= -CGFLOAT_MAX); + if ((val > 0 && val < CGFLOAT_MIN) || (val < 0 && val > -CGFLOAT_MIN)) { + NSLog(@"Warning - conversion of long double %Lf to CGFloat will result in the non-normal CGFloat %f", val, (CGFloat)val); + } + } +#endif + return (CGFloat)val; +} + +/*! Returns the quotient of a divided by b, rounding up, for unsigned long longs. Will not overflow. */ +static inline unsigned long long HFDivideULLRoundingUp(unsigned long long a, unsigned long long b) { + if (a == 0) return 0; + else return ((a - 1) / b) + 1; +} + +/*! Returns the quotient of a divided by b, rounding up, for NSUIntegers. Will not overflow. */ +static inline NSUInteger HFDivideULRoundingUp(NSUInteger a, NSUInteger b) { + if (a == 0) return 0; + else return ((a - 1) / b) + 1; +} + +/*! Draws a shadow. */ +void HFDrawShadow(CGContextRef context, NSRect rect, CGFloat size, NSRectEdge rectEdge, BOOL active, NSRect clip); + +/*! Registers a view to have the given notificationSEL invoked (taking the NSNotification object) when the window becomes or loses key. If appToo is YES, this also registers with NSApplication for Activate and Deactivate methods. */ +void HFRegisterViewForWindowAppearanceChanges(NSView *view, SEL notificationSEL, BOOL appToo); + +/*! Unregisters a view to have the given notificationSEL invoked when the window becomes or loses key. If appToo is YES, this also unregisters with NSApplication. */ +void HFUnregisterViewForWindowAppearanceChanges(NSView *view, BOOL appToo); + +/*! Returns a description of the given byte count (e.g. "24 kilobytes") */ +NSString *HFDescribeByteCount(unsigned long long count); + +/*! @brief An object wrapper for the HFRange type. + + A simple class responsible for holding an immutable HFRange as an object. Methods that logically work on multiple HFRanges usually take or return arrays of HFRangeWrappers. */ +@interface HFRangeWrapper : NSObject { + @public + HFRange range; +} + +/*! Returns the HFRange for this HFRangeWrapper. */ +- (HFRange)HFRange; + +/*! Creates an autoreleased HFRangeWrapper for this HFRange. */ ++ (HFRangeWrapper *)withRange:(HFRange)range; + +/*! Creates an NSArray of HFRangeWrappers for this HFRange. */ ++ (NSArray *)withRanges:(const HFRange *)ranges count:(NSUInteger)count; + +/*! Given an NSArray of HFRangeWrappers, get all of the HFRanges into a C array. */ ++ (void)getRanges:(HFRange *)ranges fromArray:(NSArray *)array; + +/*! Given an array of HFRangeWrappers, returns a "cleaned up" array of equivalent ranges. This new array represents the same indexes, but overlapping ranges will have been merged, and the ranges will be sorted in ascending order. */ ++ (NSArray *)organizeAndMergeRanges:(NSArray *)inputRanges; + +@end + +/*! @brief A set of HFRanges. HFRangeSet takes the interpetation that all zero-length ranges are identical. + + Essentially, a mutable array of ranges that is maintained to be sorted and minimized (i.e. merged with overlapping neighbors). + + TODO: The HexFiend codebase currently uses arrays of HFRangeWrappers that have been run through organizeAndMergeRanges:, and not HFRangeSet. The advantage of HFRangeSet is that the sorting & merging is implied by the type, instead of just tacitly assumed. This should lead to less confusion and fewer extra applications of organizeAndMergeRanges. + + TODO: HFRangeSet needs to be tested! I guarantee it has bugs! (Which doesn't matter right now because it's all dead code...) + */ +@interface HFRangeSet : NSObject { + @private + CFMutableArrayRef array; +} + +/*! Create a range set with just one range. */ ++ (HFRangeSet *)withRange:(HFRange)range; + +/*! Create a range set with a C array of ranges. No prior sorting is necessary. */ ++ (HFRangeSet *)withRanges:(const HFRange *)ranges count:(NSUInteger)count; + +/*! Create a range set with an array of HFRangeWrappers. No prior sorting is necessary. */ ++ (HFRangeSet *)withRangeWrappers:(NSArray *)ranges; + +/*! Create a range set as a copy of another. */ ++ (HFRangeSet *)withRangeSet:(HFRangeSet *)rangeSet; + +/*! Equivalent to HFRangeSet *x = [HFRangeSet withRange:range]; [x removeRange:rangeSet]; */ ++ (HFRangeSet *)complementOfRangeSet:(HFRangeSet *)rangeSet inRange:(HFRange)range; + +- (void)addRange:(HFRange)range; /*!< Union with range */ +- (void)removeRange:(HFRange)range; /*!< Subtract range */ +- (void)clipToRange:(HFRange)range; /*!< Intersect with range */ +- (void)toggleRange:(HFRange)range; /*!< Symmetric difference with range */ + +- (void)addRangeSet:(HFRangeSet *)rangeSet; /*!< Union with range set */ +- (void)removeRangeSet:(HFRangeSet *)rangeSet; /*!< Subtract range set */ +- (void)clipToRangeSet:(HFRangeSet *)rangeSet; /*!< Intersect with range set */ +- (void)toggleRangeSet:(HFRangeSet *)rangeSet; /*!< Symmetric difference with range set */ + + +- (BOOL)isEqualToRangeSet:(HFRangeSet *)rangeSet; /*!< Test if two range sets are equivalent. */ +- (BOOL)isEmpty; /*!< Test if range set is empty. */ + +- (BOOL)containsAllRange:(HFRange)range; /*!< Check if the range set covers all of a range. Always true if 'range' is zero length. */ +- (BOOL)overlapsAnyRange:(HFRange)range; /*!< Check if the range set covers any of a range. Never true if 'range' is zero length. */ +- (BOOL)containsAllRangeSet:(HFRangeSet *)rangeSet; /*!< Check if this range is a superset of another. */ +- (BOOL)overlapsAnyRangeSet:(HFRangeSet *)rangeSet; /*!< Check if this range has a nonempty intersection with another. */ + +- (HFRange)spanningRange; /*!< Return a single range that covers the entire range set */ + +- (void)assertIntegrity; + +@end + +#ifndef NDEBUG +void HFStartTiming(const char *name); +void HFStopTiming(void); +#endif diff --git a/bsnes/gb/HexFiend/HFFunctions.m b/bsnes/gb/HexFiend/HFFunctions.m new file mode 100644 index 00000000..b41a12ff --- /dev/null +++ b/bsnes/gb/HexFiend/HFFunctions.m @@ -0,0 +1,1172 @@ +#import +#import + +#import "HFFunctions_Private.h" + +#ifndef NDEBUG +//#define USE_CHUD 1 +#endif + +#ifndef USE_CHUD +#define USE_CHUD 0 +#endif + +#if USE_CHUD +#import +#endif + +NSImage *HFImageNamed(NSString *name) { + HFASSERT(name != NULL); + NSImage *image = [NSImage imageNamed:name]; + if (image == NULL) { + NSString *imagePath = [[NSBundle bundleForClass:[HFController class]] pathForResource:name ofType:@"tiff"]; + if (! imagePath) { + NSLog(@"Unable to find image named %@.tiff", name); + } + else { + image = [[NSImage alloc] initByReferencingFile:imagePath]; + if (image == nil || ! [image isValid]) { + NSLog(@"Couldn't load image at path %@", imagePath); + [image release]; + image = nil; + } + else { + [image setName:name]; + } + } + } + return image; +} + +@implementation HFRangeWrapper + +- (HFRange)HFRange { return range; } + ++ (HFRangeWrapper *)withRange:(HFRange)range { + HFRangeWrapper *result = [[self alloc] init]; + result->range = range; + return [result autorelease]; +} + ++ (NSArray *)withRanges:(const HFRange *)ranges count:(NSUInteger)count { + HFASSERT(count == 0 || ranges != NULL); + NSUInteger i; + NSArray *result; + NEW_ARRAY(HFRangeWrapper *, wrappers, count); + for (i=0; i < count; i++) wrappers[i] = [self withRange:ranges[i]]; + result = [NSArray arrayWithObjects:wrappers count:count]; + FREE_ARRAY(wrappers); + return result; +} + +- (BOOL)isEqual:(id)obj { + if (! [obj isKindOfClass:[HFRangeWrapper class]]) return NO; + else return HFRangeEqualsRange(range, [obj HFRange]); +} + +- (NSUInteger)hash { + return (NSUInteger)(range.location + (range.length << 16)); +} + +- (id)copyWithZone:(NSZone *)zone { + USE(zone); + return [self retain]; +} + +- (NSString *)description { + return HFRangeToString(range); +} + +static int hfrange_compare(const void *ap, const void *bp) { + const HFRange *a = ap; + const HFRange *b = bp; + if (a->location < b->location) return -1; + else if (a->location > b->location) return 1; + else if (a->length < b->length) return -1; + else if (a->length > b->length) return 1; + else return 0; +} + ++ (NSArray *)organizeAndMergeRanges:(NSArray *)inputRanges { + HFASSERT(inputRanges != NULL); + NSUInteger leading = 0, trailing = 0, length = [inputRanges count]; + if (length == 0) return @[]; + else if (length == 1) return [NSArray arrayWithArray:inputRanges]; + + NEW_ARRAY(HFRange, ranges, length); + [self getRanges:ranges fromArray:inputRanges]; + qsort(ranges, length, sizeof ranges[0], hfrange_compare); + leading = 0; + while (leading < length) { + leading++; + if (leading < length) { + HFRange leadRange = ranges[leading], trailRange = ranges[trailing]; + if (HFIntersectsRange(leadRange, trailRange) || HFMaxRange(leadRange) == trailRange.location || HFMaxRange(trailRange) == leadRange.location) { + ranges[trailing] = HFUnionRange(leadRange, trailRange); + } + else { + trailing++; + ranges[trailing] = ranges[leading]; + } + } + } + NSArray *result = [HFRangeWrapper withRanges:ranges count:trailing + 1]; + FREE_ARRAY(ranges); + return result; +} + ++ (void)getRanges:(HFRange *)ranges fromArray:(NSArray *)array { + HFASSERT(ranges != NULL || [array count] == 0); + if (ranges) { + FOREACH(HFRangeWrapper*, wrapper, array) *ranges++ = [wrapper HFRange]; + } +} + +@end + +@implementation HFRangeSet +// HFRangeSet is implemented as a CFMutableArray of uintptr_t "fenceposts". The array +// is even in length, sorted, duplicate free, and considered to include the ranges +// [array[0], array[1]), [array[2], array[3]), ..., [array[2n], array[2n+1]) + +CFComparisonResult uintptrComparator(const void *val1, const void *val2, void *context) { + (void)context; + uintptr_t a = (uintptr_t)val1; + uintptr_t b = (uintptr_t)val2; + if(a < b) return kCFCompareLessThan; + if(a > b) return kCFCompareGreaterThan; + return kCFCompareEqualTo; +} + +static void HFRangeSetAddRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + + const void *x[2] = { (void*)a, (void*)b }; + if(idxa >= count) { + CFArrayReplaceValues(array, CFRangeMake(count, 0), x, 2); + return; + } + if(idxb == 0) { + CFArrayReplaceValues(array, CFRangeMake(0, 0), x, 2); + return; + } + + // Clear fenceposts strictly between 'a' and 'b', and then possibly + // add 'a' or 'b' as fenceposts. + CFIndex cutloc = (uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxa+1 : idxa; + CFIndex cutlen = idxb - cutloc; + + bool inca = cutloc % 2 == 0; // Include 'a' if it would begin an included range + bool incb = (count - cutlen + inca) % 2 == 1; // The set must be even, which tells us about 'b'. + + CFArrayReplaceValues(array, CFRangeMake(cutloc, cutlen), x+inca, inca+incb); + assert(CFArrayGetCount(array) % 2 == 0); +} + +static void HFRangeSetRemoveRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) return; + + // Remove fenceposts strictly between 'a' and 'b', and then possibly + // add 'a' or 'b' as fenceposts. + CFIndex cutloc = (uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxa+1 : idxa; + CFIndex cutlen = idxb - cutloc; + + bool inca = cutloc % 2 == 1; // Include 'a' if it would end an included range + bool incb = (count - cutlen + inca) % 2 == 1; // The set must be even, which tells us about 'b'. + + const void *x[2] = { (void*)a, (void*)b }; + CFArrayReplaceValues(array, CFRangeMake(cutloc, cutlen), x+inca, inca+incb); + assert(CFArrayGetCount(array) % 2 == 0); +} + +static void HFRangeSetToggleRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + + // In the fencepost representation, simply toggling the existence of + // fenceposts 'a' and 'b' achieves symmetric difference. + + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + if((uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a) { + CFArrayRemoveValueAtIndex(array, idxa); + } else { + CFArrayInsertValueAtIndex(array, idxa, (void*)a); + } + + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if((uintptr_t)CFArrayGetValueAtIndex(array, idxb) == b) { + CFArrayRemoveValueAtIndex(array, idxb); + } else { + CFArrayInsertValueAtIndex(array, idxb, (void*)b); + } + + assert(CFArrayGetCount(array) % 2 == 0); +} + +static BOOL HFRangeSetContainsAllRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) return NO; + + // Optimization: if the indexes are far enough apart, then obviouly there's a gap. + if(idxb - idxa >= 2) return NO; + + // The first fencepost >= 'b' must end an include range, a must be in the same range. + return idxb%2 == 1 && idxa == ((uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxb-1 : idxb); +} + +static BOOL HFRangeSetOverlapsAnyRange(CFMutableArrayRef array, uintptr_t a, uintptr_t b) { + CFIndex count = CFArrayGetCount(array); + assert(a < b); assert(count % 2 == 0); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) return NO; + + // Optimization: if the indexes are far enough apart, then obviouly there's overlap. + if(idxb - idxa >= 2) return YES; + + if((uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a) { + // 'a' is an included fencepost, or instead 'b' makes it past an included fencepost. + return idxa % 2 == 0 || b > (uintptr_t)CFArrayGetValueAtIndex(array, idxa+1); + } else { + // 'a' lies in an included range, or instead 'b' makes it past an included fencepost. + return idxa % 2 == 1 || b > (uintptr_t)CFArrayGetValueAtIndex(array, idxa); + } +} + +- (instancetype)init { + if(!(self = [super init])) return nil; + array = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL); + return self; +} + +- (void)dealloc { + CFRelease(array); + [super dealloc]; +} + ++ (HFRangeSet *)withRange:(HFRange)range { + HFRangeSet *newSet = [[[HFRangeSet alloc] init] autorelease]; + if(range.length > 0) { + CFArrayAppendValue(newSet->array, (void*)ll2p(range.location)); + CFArrayAppendValue(newSet->array, (void*)ll2p(HFMaxRange(range))); + } + return newSet; +} + ++ (HFRangeSet *)withRanges:(const HFRange *)ranges count:(NSUInteger)count { + // FIXME: Stub. Don't rely on the thing we're replacing! + return [HFRangeSet withRangeWrappers:[HFRangeWrapper withRanges:ranges count:count]]; +} + ++ (HFRangeSet *)withRangeWrappers:(NSArray *)ranges { + HFRangeSet *newSet = [[[HFRangeSet alloc] init] autorelease]; + FOREACH(HFRangeWrapper *, wrapper, [HFRangeWrapper organizeAndMergeRanges:ranges]) { + if(wrapper->range.length > 0) { + CFArrayAppendValue(newSet->array, (void*)ll2p(wrapper->range.location)); + CFArrayAppendValue(newSet->array, (void*)ll2p(HFMaxRange(wrapper->range))); + } + } + return newSet; +} + ++ (HFRangeSet *)withRangeSet:(HFRangeSet *)rangeSet { + return [[rangeSet copy] autorelease]; +} + ++ (HFRangeSet *)complementOfRangeSet:(HFRangeSet *)rangeSet inRange:(HFRange)range { + if(range.length <= 0) { + // Complement in empty is... empty! + return [HFRangeSet withRange:HFZeroRange]; + } + uintptr_t a = ll2p(range.location); + uintptr_t b = ll2p(HFMaxRange(range)); + CFIndex count = CFArrayGetCount(rangeSet->array); + CFIndex idxa = CFArrayBSearchValues(rangeSet->array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(rangeSet->array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) + return [HFRangeSet withRange:range]; + + // Alright, the trivial responses are past. We'll need to build a new set. + // Given the fencepost representation of sets, we can efficiently produce an + // inverted set by just copying the fenceposts between 'a' and 'b', and then + // maybe including 'a' and 'b'. + + HFRangeSet *newSet = [[[HFRangeSet alloc] init] autorelease]; + + // newSet must contain all the fenceposts strictly between 'a' and 'b' + CFIndex copyloc = (uintptr_t)CFArrayGetValueAtIndex(rangeSet->array, idxa) == a ? idxa+1 : idxa; + CFIndex copylen = idxb - copyloc; + + // Include 'a' if it's needed to invert the parity of the copy. + if(copyloc % 2 == 0) CFArrayAppendValue(newSet->array, &a); + + CFArrayAppendArray(newSet->array, rangeSet->array, CFRangeMake(copyloc, copylen)); + + // Include 'b' if it's needed to close off the set. + if(CFArrayGetCount(newSet->array) % 2 == 1) + CFArrayAppendValue(newSet->array, &b); + + assert(CFArrayGetCount(newSet->array) % 2 == 0); + return newSet; +} + + +- (void)addRange:(HFRange)range { + if(range.length == 0) return; + HFRangeSetAddRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} +- (void)removeRange:(HFRange)range { + if(range.length == 0) return; + HFRangeSetRemoveRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} +- (void)toggleRange:(HFRange)range { + if(range.length == 0) return; + HFRangeSetToggleRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} + +- (void)clipToRange:(HFRange)range { + if(range.length <= 0) { + CFArrayRemoveAllValues(array); + return; + } + uintptr_t a = ll2p(range.location); + uintptr_t b = ll2p(HFMaxRange(range)); + CFIndex count = CFArrayGetCount(array); + CFIndex idxa = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)a, uintptrComparator, NULL); + CFIndex idxb = CFArrayBSearchValues(array, CFRangeMake(0, count), (void*)b, uintptrComparator, NULL); + if(idxa >= count || idxb == 0) { + CFArrayRemoveAllValues(array); + return; + } + + // Keep only fenceposts strictly between 'a' and 'b', and then possibly + // add 'a' or 'b' as fenceposts. + CFIndex keeploc = (uintptr_t)CFArrayGetValueAtIndex(array, idxa) == a ? idxa+1 : idxa; + CFIndex keeplen = idxb - keeploc; + + // Include 'a' if it's needed to keep the parity straight. + if(keeploc % 2 == 1) { + keeploc--; keeplen++; + CFArraySetValueAtIndex(array, keeploc, (void*)a); + } + + if(keeploc > 0) + CFArrayReplaceValues(array, CFRangeMake(0, keeploc), NULL, 0); + if(keeploc+keeplen < count) + CFArrayReplaceValues(array, CFRangeMake(0, keeplen), NULL, 0); + + // Include 'b' if it's needed to keep the length even. + if(keeplen % 2 == 1) { + CFArrayAppendValue(array, (void*)b); + } + + assert(CFArrayGetCount(array) % 2 == 0); +} + + +- (void)addRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + HFRangeSetAddRange(array, (uintptr_t)CFArrayGetValueAtIndex(a, i2), (uintptr_t)CFArrayGetValueAtIndex(a, i2+1)); + } +} +- (void)removeRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + HFRangeSetRemoveRange(array, (uintptr_t)CFArrayGetValueAtIndex(a, i2), (uintptr_t)CFArrayGetValueAtIndex(a, i2+1)); + } +} +- (void)toggleRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + HFRangeSetToggleRange(array, (uintptr_t)CFArrayGetValueAtIndex(a, i2), (uintptr_t)CFArrayGetValueAtIndex(a, i2+1)); + } +} + +- (void)clipToRangeSet:(HFRangeSet *)rangeSet { + HFRange span = [rangeSet spanningRange]; + [self clipToRange:span]; + [self removeRangeSet:[HFRangeSet complementOfRangeSet:rangeSet inRange:span]]; +} + +- (BOOL)isEqualToRangeSet:(HFRangeSet *)rangeSet { + // Because our arrays are fully normalized, this just checks for array equality. + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + if(c != CFArrayGetCount(array)) + return NO; + + // Optimization: For long arrays, check the last few first, + // since appending to ranges is probably a common usage pattern. + const CFIndex opt_end = 10; + if(c > 2*opt_end) { + for(CFIndex i = c - 2*opt_end; i < c; i++) { + if(CFArrayGetValueAtIndex(a, i) != CFArrayGetValueAtIndex(array, i)) + return NO; + } + c -= 2*opt_end; + } + + for(CFIndex i = 0; i < c; i++) { + if(CFArrayGetValueAtIndex(a, i) != CFArrayGetValueAtIndex(array, i)) + return NO; + } + + return YES; +} + +- (BOOL)isEmpty { + return CFArrayGetCount(array) == 0; +} + +- (BOOL)containsAllRange:(HFRange)range { + if(range.length == 0) return YES; + return HFRangeSetContainsAllRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} + +- (BOOL)overlapsAnyRange:(HFRange)range { + if(range.length == 0) return NO; + return HFRangeSetOverlapsAnyRange(array, ll2p(range.location), ll2p(HFMaxRange(range))); +} + +- (BOOL)containsAllRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + + // Optimization: check if containment is possible. + if(!HFRangeIsSubrangeOfRange([rangeSet spanningRange], [self spanningRange])) { + return NO; + } + + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + uintptr_t x = (uintptr_t)CFArrayGetValueAtIndex(a, i2); + uintptr_t y = (uintptr_t)CFArrayGetValueAtIndex(a, i2+1); + if(!HFRangeSetContainsAllRange(array, x, y)) return NO; + } + return YES; +} + +- (BOOL)overlapsAnyRangeSet:(HFRangeSet *)rangeSet { + CFArrayRef a = rangeSet->array; + CFIndex c = CFArrayGetCount(a); + + // Optimization: check if overlap is possible. + if(!HFIntersectsRange([rangeSet spanningRange], [self spanningRange])) { + return NO; + } + + for(CFIndex i2 = 0; i2 < c; i2 += 2) { + uintptr_t x = (uintptr_t)CFArrayGetValueAtIndex(a, i2); + uintptr_t y = (uintptr_t)CFArrayGetValueAtIndex(a, i2+1); + if(!HFRangeSetOverlapsAnyRange(array, x, y)) return YES; + } + return NO; +} + + +- (HFRange)spanningRange { + CFIndex count = CFArrayGetCount(array); + if(count == 0) return HFZeroRange; + + uintptr_t a = (uintptr_t)CFArrayGetValueAtIndex(array, 0); + uintptr_t b = (uintptr_t)CFArrayGetValueAtIndex(array, count-2) + (uintptr_t)CFArrayGetValueAtIndex(array, count-1); + + return HFRangeMake(a, b-a); +} + +- (void)assertIntegrity { + CFIndex count = CFArrayGetCount(array); + HFASSERT(count % 2 == 0); + if(count == 0) return; + + uintptr_t prev = (uintptr_t)CFArrayGetValueAtIndex(array, 0); + for(CFIndex i = 1; i < count; i++) { + uintptr_t val = (uintptr_t)CFArrayGetValueAtIndex(array, i); + HFASSERT(val > prev); + prev = val; + } +} + +- (BOOL)isEqual:(id)object { + if(![object isKindOfClass:[HFRangeSet class]]) + return false; + return [self isEqualToRangeSet:object]; +} + +- (NSUInteger)hash { + CFIndex count = CFArrayGetCount(array); + NSUInteger x = 0; + for(CFIndex i2 = 0; i2 < count; i2 += 2) { + uintptr_t a = (uintptr_t)CFArrayGetValueAtIndex(array, i2); + uintptr_t b = (uintptr_t)CFArrayGetValueAtIndex(array, i2+1); +#if 6364136223846793005 < NSUIntegerMax + x = (6364136223846793005 * (uint64_t)x + a); +#else + x = (NSUInteger)(1103515245 * (uint64_t)x + a); +#endif + x ^= (NSUInteger)b; + } + return x; +} + +- (id)copyWithZone:(NSZone *)zone { + HFRangeSet *newSet = [[HFRangeSet allocWithZone:zone] init]; + CFRelease(newSet->array); + newSet->array = (CFMutableArrayRef)[[NSMutableArray allocWithZone:zone] initWithArray:(NSArray*)array copyItems:NO]; + return newSet; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + NSUInteger count = CFArrayGetCount(array); + NEW_ARRAY(uint64_t, values, count); + + // Fill array with 64-bit, little endian bytes. + if(sizeof(const void *) == sizeof(uint64_t)) { + // Hooray, we can just use CFArrayGetValues + CFArrayGetValues(array, CFRangeMake(0, count), (const void **)&values); +#if __LITTLE_ENDIAN__ +#else + // Boo, we have to swap everything. + for(NSUInteger i = 0; i < count; i++) { + values[i] = CFSwapInt64HostToLittle(values[i]); + } +#endif + } else { + // Boo, we have to iterate through the array. + NSUInteger i = 0; + FOREACH(id, val, (NSArray*)array) { + values[i++] = CFSwapInt64HostToLittle((uint64_t)(const void *)val); + } + } + [aCoder encodeBytes:values length:count * sizeof(*values)]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if(!(self = [super init])) return nil; + + NSUInteger count; + uint64_t *values = [aDecoder decodeBytesWithReturnedLength:&count]; + array = CFArrayCreateMutable(kCFAllocatorDefault, count+1, NULL); + + for(NSUInteger i = 0; i < count; i++) { + uint64_t x = CFSwapInt64LittleToHost(values[i]); + if(x > UINTPTR_MAX) + goto fail; + CFArrayAppendValue(array, (const void *)(uintptr_t)x); + } + if(CFArrayGetCount(array)%2 != 0) + goto fail; + return self; + +fail: + CFRelease(array); + [super release]; + return nil; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len { + NSUInteger base = state->state; + NSUInteger length = CFArrayGetCount(array)/2; + NSUInteger i = 0; + + while(i < len && base + i < length) { + uintptr_t a = (uintptr_t)CFArrayGetValueAtIndex(array, 2*i); + uintptr_t b = (uintptr_t)CFArrayGetValueAtIndex(array, 2*i+1); + stackbuf[i] = [HFRangeWrapper withRange:HFRangeMake(a, b-a)]; + } + + state->state = base + i; + state->itemsPtr = stackbuf; + state->mutationsPtr = &state->extra[0]; // Use simple mutation checking. + state->extra[0] = length; + + return i; +} + +@end + + +BOOL HFStringEncodingIsSupersetOfASCII(NSStringEncoding encoding) { + switch (CFStringConvertNSStringEncodingToEncoding(encoding)) { + case kCFStringEncodingMacRoman: return YES; + case kCFStringEncodingWindowsLatin1: return YES; + case kCFStringEncodingISOLatin1: return YES; + case kCFStringEncodingNextStepLatin: return YES; + case kCFStringEncodingASCII: return YES; + case kCFStringEncodingUnicode: return NO; + case kCFStringEncodingUTF8: return YES; + case kCFStringEncodingNonLossyASCII: return NO; +// case kCFStringEncodingUTF16: return NO; + case kCFStringEncodingUTF16BE: return NO; + case kCFStringEncodingUTF16LE: return NO; + case kCFStringEncodingUTF32: return NO; + case kCFStringEncodingUTF32BE: return NO; + case kCFStringEncodingUTF32LE: return NO; + case kCFStringEncodingMacJapanese: return NO; + case kCFStringEncodingMacChineseTrad: return YES; + case kCFStringEncodingMacKorean: return YES; + case kCFStringEncodingMacArabic: return NO; + case kCFStringEncodingMacHebrew: return NO; + case kCFStringEncodingMacGreek: return YES; + case kCFStringEncodingMacCyrillic: return YES; + case kCFStringEncodingMacDevanagari: return YES; + case kCFStringEncodingMacGurmukhi: return YES; + case kCFStringEncodingMacGujarati: return YES; + case kCFStringEncodingMacOriya: return YES; + case kCFStringEncodingMacBengali: return YES; + case kCFStringEncodingMacTamil: return YES; + case kCFStringEncodingMacTelugu: return YES; + case kCFStringEncodingMacKannada: return YES; + case kCFStringEncodingMacMalayalam: return YES; + case kCFStringEncodingMacSinhalese: return YES; + case kCFStringEncodingMacBurmese: return YES; + case kCFStringEncodingMacKhmer: return YES; + case kCFStringEncodingMacThai: return YES; + case kCFStringEncodingMacLaotian: return YES; + case kCFStringEncodingMacGeorgian: return YES; + case kCFStringEncodingMacArmenian: return YES; + case kCFStringEncodingMacChineseSimp: return YES; + case kCFStringEncodingMacTibetan: return YES; + case kCFStringEncodingMacMongolian: return YES; + case kCFStringEncodingMacEthiopic: return YES; + case kCFStringEncodingMacCentralEurRoman: return YES; + case kCFStringEncodingMacVietnamese: return YES; + case kCFStringEncodingMacExtArabic: return YES; + case kCFStringEncodingMacSymbol: return NO; + case kCFStringEncodingMacDingbats: return NO; + case kCFStringEncodingMacTurkish: return YES; + case kCFStringEncodingMacCroatian: return YES; + case kCFStringEncodingMacIcelandic: return YES; + case kCFStringEncodingMacRomanian: return YES; + case kCFStringEncodingMacCeltic: return YES; + case kCFStringEncodingMacGaelic: return YES; + case kCFStringEncodingMacFarsi: return YES; + case kCFStringEncodingMacUkrainian: return NO; + case kCFStringEncodingMacInuit: return YES; + case kCFStringEncodingMacVT100: return YES; + case kCFStringEncodingMacHFS: return YES; + case kCFStringEncodingISOLatin2: return YES; + case kCFStringEncodingISOLatin3: return YES; + case kCFStringEncodingISOLatin4: return YES; + case kCFStringEncodingISOLatinCyrillic: return YES; + case kCFStringEncodingISOLatinArabic: return NO; + case kCFStringEncodingISOLatinGreek: return YES; + case kCFStringEncodingISOLatinHebrew: return YES; + case kCFStringEncodingISOLatin5: return YES; + case kCFStringEncodingISOLatin6: return YES; + case kCFStringEncodingISOLatinThai: return YES; + case kCFStringEncodingISOLatin7: return YES; + case kCFStringEncodingISOLatin8: return YES; + case kCFStringEncodingISOLatin9: return YES; + case kCFStringEncodingISOLatin10: return YES; + case kCFStringEncodingDOSLatinUS: return YES; + case kCFStringEncodingDOSGreek: return YES; + case kCFStringEncodingDOSBalticRim: return YES; + case kCFStringEncodingDOSLatin1: return YES; + case kCFStringEncodingDOSGreek1: return YES; + case kCFStringEncodingDOSLatin2: return YES; + case kCFStringEncodingDOSCyrillic: return YES; + case kCFStringEncodingDOSTurkish: return YES; + case kCFStringEncodingDOSPortuguese: return YES; + case kCFStringEncodingDOSIcelandic: return YES; + case kCFStringEncodingDOSHebrew: return YES; + case kCFStringEncodingDOSCanadianFrench: return YES; + case kCFStringEncodingDOSArabic: return YES; + case kCFStringEncodingDOSNordic: return YES; + case kCFStringEncodingDOSRussian: return YES; + case kCFStringEncodingDOSGreek2: return YES; + case kCFStringEncodingDOSThai: return YES; + case kCFStringEncodingDOSJapanese: return YES; + case kCFStringEncodingDOSChineseSimplif: return YES; + case kCFStringEncodingDOSKorean: return YES; + case kCFStringEncodingDOSChineseTrad: return YES; + case kCFStringEncodingWindowsLatin2: return YES; + case kCFStringEncodingWindowsCyrillic: return YES; + case kCFStringEncodingWindowsGreek: return YES; + case kCFStringEncodingWindowsLatin5: return YES; + case kCFStringEncodingWindowsHebrew: return YES; + case kCFStringEncodingWindowsArabic: return YES; + case kCFStringEncodingWindowsBalticRim: return YES; + case kCFStringEncodingWindowsVietnamese: return YES; + case kCFStringEncodingWindowsKoreanJohab: return YES; + case kCFStringEncodingANSEL: return NO; + case kCFStringEncodingJIS_X0201_76: return NO; + case kCFStringEncodingJIS_X0208_83: return NO; + case kCFStringEncodingJIS_X0208_90: return NO; + case kCFStringEncodingJIS_X0212_90: return NO; + case kCFStringEncodingJIS_C6226_78: return NO; + case 0x0628/*kCFStringEncodingShiftJIS_X0213*/: return NO; + case kCFStringEncodingShiftJIS_X0213_MenKuTen: return NO; + case kCFStringEncodingGB_2312_80: return NO; + case kCFStringEncodingGBK_95: return NO; + case kCFStringEncodingGB_18030_2000: return NO; + case kCFStringEncodingKSC_5601_87: return NO; + case kCFStringEncodingKSC_5601_92_Johab: return NO; + case kCFStringEncodingCNS_11643_92_P1: return NO; + case kCFStringEncodingCNS_11643_92_P2: return NO; + case kCFStringEncodingCNS_11643_92_P3: return NO; + case kCFStringEncodingISO_2022_JP: return NO; + case kCFStringEncodingISO_2022_JP_2: return NO; + case kCFStringEncodingISO_2022_JP_1: return NO; + case kCFStringEncodingISO_2022_JP_3: return NO; + case kCFStringEncodingISO_2022_CN: return NO; + case kCFStringEncodingISO_2022_CN_EXT: return NO; + case kCFStringEncodingISO_2022_KR: return NO; + case kCFStringEncodingEUC_JP: return YES; + case kCFStringEncodingEUC_CN: return YES; + case kCFStringEncodingEUC_TW: return YES; + case kCFStringEncodingEUC_KR: return YES; + case kCFStringEncodingShiftJIS: return NO; + case kCFStringEncodingKOI8_R: return YES; + case kCFStringEncodingBig5: return YES; + case kCFStringEncodingMacRomanLatin1: return YES; + case kCFStringEncodingHZ_GB_2312: return NO; + case kCFStringEncodingBig5_HKSCS_1999: return YES; + case kCFStringEncodingVISCII: return YES; // though not quite + case kCFStringEncodingKOI8_U: return YES; + case kCFStringEncodingBig5_E: return YES; + case kCFStringEncodingNextStepJapanese: return YES; + case kCFStringEncodingEBCDIC_US: return NO; + case kCFStringEncodingEBCDIC_CP037: return NO; + default: + NSLog(@"Unknown string encoding %lu in %s", (unsigned long)encoding, __FUNCTION__); + return NO; + } +} + +uint8_t HFStringEncodingCharacterLength(NSStringEncoding encoding) { + switch (CFStringConvertNSStringEncodingToEncoding(encoding)) { + case kCFStringEncodingMacRoman: return 1; + case kCFStringEncodingWindowsLatin1: return 1; + case kCFStringEncodingISOLatin1: return 1; + case kCFStringEncodingNextStepLatin: return 1; + case kCFStringEncodingASCII: return 1; + case kCFStringEncodingUnicode: return 2; + case kCFStringEncodingUTF8: return 1; + case kCFStringEncodingNonLossyASCII: return 1; + // case kCFStringEncodingUTF16: return 2; + case kCFStringEncodingUTF16BE: return 2; + case kCFStringEncodingUTF16LE: return 2; + case kCFStringEncodingUTF32: return 4; + case kCFStringEncodingUTF32BE: return 4; + case kCFStringEncodingUTF32LE: return 4; + case kCFStringEncodingMacJapanese: return 1; + case kCFStringEncodingMacChineseTrad: return 1; // ?? + case kCFStringEncodingMacKorean: return 1; + case kCFStringEncodingMacArabic: return 1; + case kCFStringEncodingMacHebrew: return 1; + case kCFStringEncodingMacGreek: return 1; + case kCFStringEncodingMacCyrillic: return 1; + case kCFStringEncodingMacDevanagari: return 1; + case kCFStringEncodingMacGurmukhi: return 1; + case kCFStringEncodingMacGujarati: return 1; + case kCFStringEncodingMacOriya: return 1; + case kCFStringEncodingMacBengali: return 1; + case kCFStringEncodingMacTamil: return 1; + case kCFStringEncodingMacTelugu: return 1; + case kCFStringEncodingMacKannada: return 1; + case kCFStringEncodingMacMalayalam: return 1; + case kCFStringEncodingMacSinhalese: return 1; + case kCFStringEncodingMacBurmese: return 1; + case kCFStringEncodingMacKhmer: return 1; + case kCFStringEncodingMacThai: return 1; + case kCFStringEncodingMacLaotian: return 1; + case kCFStringEncodingMacGeorgian: return 1; + case kCFStringEncodingMacArmenian: return 1; + case kCFStringEncodingMacChineseSimp: return 1; + case kCFStringEncodingMacTibetan: return 1; + case kCFStringEncodingMacMongolian: return 1; + case kCFStringEncodingMacEthiopic: return 1; + case kCFStringEncodingMacCentralEurRoman: return 1; + case kCFStringEncodingMacVietnamese: return 1; + case kCFStringEncodingMacExtArabic: return 1; + case kCFStringEncodingMacSymbol: return 1; + case kCFStringEncodingMacDingbats: return 1; + case kCFStringEncodingMacTurkish: return 1; + case kCFStringEncodingMacCroatian: return 1; + case kCFStringEncodingMacIcelandic: return 1; + case kCFStringEncodingMacRomanian: return 1; + case kCFStringEncodingMacCeltic: return 1; + case kCFStringEncodingMacGaelic: return 1; + case kCFStringEncodingMacFarsi: return 1; + case kCFStringEncodingMacUkrainian: return 1; + case kCFStringEncodingMacInuit: return 1; + case kCFStringEncodingMacVT100: return 1; + case kCFStringEncodingMacHFS: return 1; + case kCFStringEncodingISOLatin2: return 1; + case kCFStringEncodingISOLatin3: return 1; + case kCFStringEncodingISOLatin4: return 1; + case kCFStringEncodingISOLatinCyrillic: return 1; + case kCFStringEncodingISOLatinArabic: return 1; + case kCFStringEncodingISOLatinGreek: return 1; + case kCFStringEncodingISOLatinHebrew: return 1; + case kCFStringEncodingISOLatin5: return 1; + case kCFStringEncodingISOLatin6: return 1; + case kCFStringEncodingISOLatinThai: return 1; + case kCFStringEncodingISOLatin7: return 1; + case kCFStringEncodingISOLatin8: return 1; + case kCFStringEncodingISOLatin9: return 1; + case kCFStringEncodingISOLatin10: return 1; + case kCFStringEncodingDOSLatinUS: return 1; + case kCFStringEncodingDOSGreek: return 1; + case kCFStringEncodingDOSBalticRim: return 1; + case kCFStringEncodingDOSLatin1: return 1; + case kCFStringEncodingDOSGreek1: return 1; + case kCFStringEncodingDOSLatin2: return 1; + case kCFStringEncodingDOSCyrillic: return 1; + case kCFStringEncodingDOSTurkish: return 1; + case kCFStringEncodingDOSPortuguese: return 1; + case kCFStringEncodingDOSIcelandic: return 1; + case kCFStringEncodingDOSHebrew: return 1; + case kCFStringEncodingDOSCanadianFrench: return 1; + case kCFStringEncodingDOSArabic: return 1; + case kCFStringEncodingDOSNordic: return 1; + case kCFStringEncodingDOSRussian: return 1; + case kCFStringEncodingDOSGreek2: return 1; + case kCFStringEncodingDOSThai: return 1; + case kCFStringEncodingDOSJapanese: return 1; + case kCFStringEncodingDOSChineseSimplif: return 1; + case kCFStringEncodingDOSKorean: return 1; + case kCFStringEncodingDOSChineseTrad: return 1; + case kCFStringEncodingWindowsLatin2: return 1; + case kCFStringEncodingWindowsCyrillic: return 1; + case kCFStringEncodingWindowsGreek: return 1; + case kCFStringEncodingWindowsLatin5: return 1; + case kCFStringEncodingWindowsHebrew: return 1; + case kCFStringEncodingWindowsArabic: return 1; + case kCFStringEncodingWindowsBalticRim: return 1; + case kCFStringEncodingWindowsVietnamese: return 1; + case kCFStringEncodingWindowsKoreanJohab: return 1; + case kCFStringEncodingANSEL: return 1; + case kCFStringEncodingJIS_X0201_76: return 1; + case kCFStringEncodingJIS_X0208_83: return 1; + case kCFStringEncodingJIS_X0208_90: return 1; + case kCFStringEncodingJIS_X0212_90: return 1; + case kCFStringEncodingJIS_C6226_78: return 1; + case 0x0628/*kCFStringEncodingShiftJIS_X0213*/: return 1; + case kCFStringEncodingShiftJIS_X0213_MenKuTen: return 1; + case kCFStringEncodingGB_2312_80: return 1; + case kCFStringEncodingGBK_95: return 1; + case kCFStringEncodingGB_18030_2000: return 1; + case kCFStringEncodingKSC_5601_87: return 1; + case kCFStringEncodingKSC_5601_92_Johab: return 1; + case kCFStringEncodingCNS_11643_92_P1: return 1; + case kCFStringEncodingCNS_11643_92_P2: return 1; + case kCFStringEncodingCNS_11643_92_P3: return 1; + case kCFStringEncodingISO_2022_JP: return 1; + case kCFStringEncodingISO_2022_JP_2: return 1; + case kCFStringEncodingISO_2022_JP_1: return 1; + case kCFStringEncodingISO_2022_JP_3: return 1; + case kCFStringEncodingISO_2022_CN: return 1; + case kCFStringEncodingISO_2022_CN_EXT: return 1; + case kCFStringEncodingISO_2022_KR: return 1; + case kCFStringEncodingEUC_JP: return 1; + case kCFStringEncodingEUC_CN: return 1; + case kCFStringEncodingEUC_TW: return 1; + case kCFStringEncodingEUC_KR: return 1; + case kCFStringEncodingShiftJIS: return 1; + case kCFStringEncodingKOI8_R: return 1; + case kCFStringEncodingBig5: return 2; //yay, a 2 + case kCFStringEncodingMacRomanLatin1: return 1; + case kCFStringEncodingHZ_GB_2312: return 2; + case kCFStringEncodingBig5_HKSCS_1999: return 1; + case kCFStringEncodingVISCII: return 1; + case kCFStringEncodingKOI8_U: return 1; + case kCFStringEncodingBig5_E: return 2; + case kCFStringEncodingNextStepJapanese: return YES; // ?? + case kCFStringEncodingEBCDIC_US: return 1; //lol + case kCFStringEncodingEBCDIC_CP037: return 1; + case kCFStringEncodingUTF7: return 1; + case kCFStringEncodingUTF7_IMAP : return 1; + default: + NSLog(@"Unknown string encoding %lx in %s", (long)encoding, __FUNCTION__); + return 1; + } +} + +/* Converts a hexadecimal digit into a corresponding 4 bit unsigned int; returns -1 on failure. The ... is a gcc extension. */ +static NSInteger char2hex(unichar c) { + switch (c) { + case '0' ... '9': return c - '0'; + case 'a' ... 'f': return c - 'a' + 10; + case 'A' ... 'F': return c - 'A' + 10; + default: return -1; + } +} + +static unsigned char hex2char(NSUInteger c) { + HFASSERT(c < 16); + return "0123456789ABCDEF"[c]; +} + +NSData *HFDataFromHexString(NSString *string, BOOL* isMissingLastNybble) { + REQUIRE_NOT_NULL(string); + NSUInteger stringIndex=0, resultIndex=0, max=[string length]; + NSMutableData* result = [NSMutableData dataWithLength:(max + 1)/2]; + unsigned char* bytes = [result mutableBytes]; + + NSUInteger numNybbles = 0; + unsigned char byteValue = 0; + + for (stringIndex = 0; stringIndex < max; stringIndex++) { + NSInteger val = char2hex([string characterAtIndex:stringIndex]); + if (val < 0) continue; + numNybbles++; + byteValue = byteValue * 16 + (unsigned char)val; + if (! (numNybbles % 2)) { + bytes[resultIndex++] = byteValue; + byteValue = 0; + } + } + + if (isMissingLastNybble) *isMissingLastNybble = (numNybbles % 2); + + //final nibble + if (numNybbles % 2) { + bytes[resultIndex++] = byteValue; + } + + [result setLength:resultIndex]; + return result; +} + +NSString *HFHexStringFromData(NSData *data) { + REQUIRE_NOT_NULL(data); + NSUInteger dataLength = [data length]; + NSUInteger stringLength = HFProductInt(dataLength, 2); + const unsigned char *bytes = [data bytes]; + unsigned char *charBuffer = check_malloc(stringLength); + NSUInteger charIndex = 0, byteIndex; + for (byteIndex = 0; byteIndex < dataLength; byteIndex++) { + unsigned char byte = bytes[byteIndex]; + charBuffer[charIndex++] = hex2char(byte >> 4); + charBuffer[charIndex++] = hex2char(byte & 0xF); + } + return [[[NSString alloc] initWithBytesNoCopy:charBuffer length:stringLength encoding:NSASCIIStringEncoding freeWhenDone:YES] autorelease]; +} + +void HFSetFDShouldCache(int fd, BOOL shouldCache) { + int result = fcntl(fd, F_NOCACHE, !shouldCache); + if (result == -1) { + int err = errno; + NSLog(@"fcntl(%d, F_NOCACHE, %d) returned error %d: %s", fd, !shouldCache, err, strerror(err)); + } +} + +NSString *HFDescribeByteCount(unsigned long long count) { + return HFDescribeByteCountWithPrefixAndSuffix(NULL, count, NULL); +} + +/* A big_num represents a number in some base. Here it is value = big * base + little. */ +typedef struct big_num { + unsigned int big; + unsigned long long little; +} big_num; + +static inline big_num divide_bignum_by_2(big_num a, unsigned long long base) { + //value = a.big * base + a.little; + big_num result; + result.big = a.big / 2; + unsigned int shiftedRemainder = (unsigned int)(a.little & 1); + result.little = a.little / 2; + if (a.big & 1) { + //need to add base/2 to result.little. We know that won't overflow because result.little is already a.little / 2 + result.little += base / 2; + + // If we shift off a bit for base/2, and we also shifted off a bit for a.little/2, then we have a carry bit we need to add + if ((base & 1) && shiftedRemainder) { + /* Is there a chance that adding 1 will overflow? We know base is odd (base & 1), so consider an example of base = 9. Then the largest that result.little could be is (9 - 1)/2 + base/2 = 8. We could add 1 and get back to base, but we can never exceed base, so we cannot overflow an unsigned long long. */ + result.little += 1; + HFASSERT(result.little <= base); + if (result.little == base) { + result.big++; + result.little = 0; + } + } + } + HFASSERT(result.little < base); + return result; +} + +static inline big_num add_big_nums(big_num a, big_num b, unsigned long long base) { + /* Perform the addition result += left. The addition is: + result.big = a.big + b.big + (a.little + b.little) / base + result.little = (a.little + b.little) % base + + a.little + b.little may overflow, so we have to take some care in how we calculate them. + Since both a.little and b.little are less than base, we know that if we overflow, we can subtract base from it to underflow and still get the same remainder. + */ + unsigned long long remainder = a.little + b.little; + unsigned int dividend = 0; + // remainder < a.little detects overflow, and remainder >= base detects the case where we did not overflow but are larger than base + if (remainder < a.little || remainder >= base) { + remainder -= base; + dividend++; + } + HFASSERT(remainder < base); + + big_num result = {a.big + b.big + dividend, remainder}; + return result; +} + + +/* Returns the first digit after the decimal point for a / b, rounded off, without overflow. This may return 10, indicating that the digit is 0 and we should carry. */ +static unsigned int computeRemainderPrincipalDigit(unsigned long long a, unsigned long long base) { + struct big_num result = {0, 0}, left = {(unsigned)(a / base), a % base}, right = {(unsigned)(100 / base), 100 % base}; + while (right.big > 0 || right.little > 0) { + /* Determine the least significant bit of right, which is right.big * base + right.little */ + unsigned int bigTermParity = (base & 1) && (right.big & 1); + unsigned int littleTermParity = (unsigned)(right.little & 1); + if (bigTermParity != littleTermParity) result = add_big_nums(result, left, base); + + right = divide_bignum_by_2(right, base); + left = add_big_nums(left, left, base); + } + + //result.big now contains 100 * a / base + unsigned int principalTwoDigits = (unsigned int)(result.big % 100); + unsigned int principalDigit = (principalTwoDigits / 10) + ((principalTwoDigits % 10) >= 5); + return principalDigit; +} + +NSString *HFDescribeByteCountWithPrefixAndSuffix(const char *stringPrefix, unsigned long long count, const char *stringSuffix) { + if (! stringPrefix) stringPrefix = ""; + if (! stringSuffix) stringSuffix = ""; + + if (count == 0) return [NSString stringWithFormat:@"%s0 bytes%s", stringPrefix, stringSuffix]; + + const struct { + unsigned long long size; + const char *suffix; + } suffixes[] = { + {1ULL<<0, "byte"}, + {1ULL<<10, "byte"}, + {1ULL<<20, "kilobyte"}, + {1ULL<<30, "megabyte"}, + {1ULL<<40, "gigabyte"}, + {1ULL<<50, "terabyte"}, + {1ULL<<60, "petabyte"}, + {ULLONG_MAX, "exabyte"} + }; + const unsigned numSuffixes = sizeof suffixes / sizeof *suffixes; + //HFASSERT((sizeof sizes / sizeof *sizes) == (sizeof suffixes / sizeof *suffixes)); + unsigned i; + unsigned long long base; + for (i=0; i < numSuffixes; i++) { + if (count < suffixes[i].size || suffixes[i].size == ULLONG_MAX) break; + } + + if (i >= numSuffixes) return [NSString stringWithFormat:@"%san unbelievable number of bytes%s", stringPrefix, stringSuffix]; + base = suffixes[i-1].size; + + unsigned long long dividend = count / base; + unsigned int remainderPrincipalDigit = computeRemainderPrincipalDigit(count % base, base); + HFASSERT(remainderPrincipalDigit <= 10); + if (remainderPrincipalDigit == 10) { + /* Carry */ + dividend++; + remainderPrincipalDigit = 0; + } + + BOOL needsPlural = (dividend != 1 || remainderPrincipalDigit > 0); + + char remainderBuff[64]; + if (remainderPrincipalDigit > 0) snprintf(remainderBuff, sizeof remainderBuff, ".%u", remainderPrincipalDigit); + else remainderBuff[0] = 0; + + char* resultPointer = NULL; + int numChars = asprintf(&resultPointer, "%s%llu%s %s%s%s", stringPrefix, dividend, remainderBuff, suffixes[i].suffix, needsPlural ? "s" : "", stringSuffix); + if (numChars < 0) return NULL; + return [[[NSString alloc] initWithBytesNoCopy:resultPointer length:numChars encoding:NSASCIIStringEncoding freeWhenDone:YES] autorelease]; +} + +static CGFloat interpolateShadow(CGFloat val) { + //A value of 1 means we are at the rightmost, and should return our max value. By adjusting the scale, we control how quickly the shadow drops off. + CGFloat scale = 1.4; + return (CGFloat)(expm1(val * scale) / expm1(scale)); +} + +void HFDrawShadow(CGContextRef ctx, NSRect rect, CGFloat shadowSize, NSRectEdge rectEdge, BOOL drawActive, NSRect clip) { + NSRect remainingRect, unused; + NSDivideRect(rect, &remainingRect, &unused, shadowSize, rectEdge); + + CGFloat maxAlpha = (drawActive ? .25 : .10); + + for (CGFloat i=0; i < shadowSize; i++) { + NSRect shadowLine; + NSDivideRect(remainingRect, &shadowLine, &remainingRect, 1, rectEdge); + + NSRect clippedLine = NSIntersectionRect(shadowLine, clip); + if (! NSIsEmptyRect(clippedLine)) { + CGFloat gray = 0.; + CGFloat alpha = maxAlpha * interpolateShadow((shadowSize - i) / shadowSize); + CGContextSetGrayFillColor(ctx, gray, alpha); + CGContextFillRect(ctx, NSRectToCGRect(clippedLine)); + } + } + +} + +void HFRegisterViewForWindowAppearanceChanges(NSView *self, SEL notificationSEL, BOOL appToo) { + NSWindow *window = [self window]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + if (window) { + [center addObserver:self selector:notificationSEL name:NSWindowDidBecomeKeyNotification object:window]; + [center addObserver:self selector:notificationSEL name:NSWindowDidResignKeyNotification object:window]; + } + if (appToo) { + [center addObserver:self selector:notificationSEL name:NSApplicationDidBecomeActiveNotification object:nil]; + [center addObserver:self selector:notificationSEL name:NSApplicationDidResignActiveNotification object:nil]; + } +} + +void HFUnregisterViewForWindowAppearanceChanges(NSView *self, BOOL appToo) { + NSWindow *window = [self window]; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + if (window) { + [center removeObserver:self name:NSWindowDidBecomeKeyNotification object:window]; + [center removeObserver:self name:NSWindowDidResignKeyNotification object:window]; + } + if (appToo) { + [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil]; + [center removeObserver:self name:NSApplicationDidResignActiveNotification object:nil]; + } +} + +#if USE_CHUD +void HFStartTiming(const char *name) { + static BOOL inited; + if (! inited) { + inited = YES; + chudInitialize(); + chudSetErrorLogFile(stderr); + chudAcquireRemoteAccess(); + } + chudStartRemotePerfMonitor(name); + +} + +void HFStopTiming(void) { + chudStopRemotePerfMonitor(); +} +#else +void HFStartTiming(const char *name) { USE(name); } +void HFStopTiming(void) { } +#endif diff --git a/bsnes/gb/HexFiend/HFFunctions_Private.h b/bsnes/gb/HexFiend/HFFunctions_Private.h new file mode 100644 index 00000000..3595d3da --- /dev/null +++ b/bsnes/gb/HexFiend/HFFunctions_Private.h @@ -0,0 +1,52 @@ +#import + +@class HFController; + +static inline BOOL HFIsRunningOnMountainLionOrLater(void) { + return NSAppKitVersionNumber >= NSAppKitVersionNumber10_8; +} + +/* Returns the first index where the strings differ. If the strings do not differ in any characters but are of different lengths, returns the smaller length; if they are the same length and do not differ, returns NSUIntegerMax */ +static inline NSUInteger HFIndexOfFirstByteThatDiffers(const unsigned char *a, NSUInteger len1, const unsigned char *b, NSUInteger len2) { + NSUInteger endIndex = MIN(len1, len2); + for (NSUInteger i = 0; i < endIndex; i++) { + if (a[i] != b[i]) return i; + } + if (len1 != len2) return endIndex; + return NSUIntegerMax; +} + +/* Returns the last index where the strings differ. If the strings do not differ in any characters but are of different lengths, returns the larger length; if they are the same length and do not differ, returns NSUIntegerMax */ +static inline NSUInteger HFIndexOfLastByteThatDiffers(const unsigned char *a, NSUInteger len1, const unsigned char *b, NSUInteger len2) { + if (len1 != len2) return MAX(len1, len2); + NSUInteger i = len1; + while (i--) { + if (a[i] != b[i]) return i; + } + return NSUIntegerMax; +} + +static inline unsigned long long llmin(unsigned long long a, unsigned long long b) { + return a < b ? a : b; +} + +__private_extern__ NSImage *HFImageNamed(NSString *name); + +/* Returns an NSData from an NSString containing hexadecimal characters. Characters that are not hexadecimal digits are silently skipped. Returns by reference whether the last byte contains only one nybble, in which case it will be returned in the low 4 bits of the last byte. */ +__private_extern__ NSData *HFDataFromHexString(NSString *string, BOOL* isMissingLastNybble); + +__private_extern__ NSString *HFHexStringFromData(NSData *data); + +/* Modifies F_NOCACHE for a given file descriptor */ +__private_extern__ void HFSetFDShouldCache(int fd, BOOL shouldCache); + +__private_extern__ NSString *HFDescribeByteCountWithPrefixAndSuffix(const char *stringPrefix, unsigned long long count, const char *stringSuffix); + +/* Function for OSAtomicAdd64 that just does a non-atomic add on PowerPC. This should not be used where atomicity is critical; an example where this is used is updating a progress bar. */ +static inline int64_t HFAtomicAdd64(int64_t a, volatile int64_t *b) { +#if __ppc__ + return *b += a; +#else + return OSAtomicAdd64(a, b); +#endif +} diff --git a/bsnes/gb/HexFiend/HFGlyphTrie.h b/bsnes/gb/HexFiend/HFGlyphTrie.h new file mode 100644 index 00000000..36d4b08c --- /dev/null +++ b/bsnes/gb/HexFiend/HFGlyphTrie.h @@ -0,0 +1,49 @@ +/* HFGlyphTrie is used to represent a trie of glyphs that allows multiple concurrent readers, along with one writer. */ + +#import + +/* BranchFactor is in bits */ +#define kHFGlyphTrieBranchFactor 4 +#define kHFGlyphTrieBranchCount (1 << kHFGlyphTrieBranchFactor) + +typedef uint16_t HFGlyphFontIndex; +#define kHFGlyphFontIndexInvalid ((HFGlyphFontIndex)(-1)) + +#define kHFGlyphInvalid kCGFontIndexInvalid + +struct HFGlyph_t { + HFGlyphFontIndex fontIndex; + CGGlyph glyph; +}; + +static inline BOOL HFGlyphEqualsGlyph(struct HFGlyph_t a, struct HFGlyph_t b) __attribute__((unused)); +static inline BOOL HFGlyphEqualsGlyph(struct HFGlyph_t a, struct HFGlyph_t b) { + return a.glyph == b.glyph && a.fontIndex == b.fontIndex; +} + +struct HFGlyphTrieBranch_t { + __strong void *children[kHFGlyphTrieBranchCount]; +}; + +struct HFGlyphTrieLeaf_t { + struct HFGlyph_t glyphs[kHFGlyphTrieBranchCount]; +}; + +struct HFGlyphTrie_t { + uint8_t branchingDepth; + struct HFGlyphTrieBranch_t root; +}; + +/* Initializes a trie witha given key size */ +__private_extern__ void HFGlyphTrieInitialize(struct HFGlyphTrie_t *trie, uint8_t keySize); + +/* Inserts a glyph into the trie */ +__private_extern__ void HFGlyphTrieInsert(struct HFGlyphTrie_t *trie, NSUInteger key, struct HFGlyph_t value); + +/* Attempts to fetch a glyph. If the glyph is not present, returns an HFGlyph_t set to all bits 0. */ +__private_extern__ struct HFGlyph_t HFGlyphTrieGet(const struct HFGlyphTrie_t *trie, NSUInteger key); + +/* Frees all storage associated with a glyph tree. This is not necessary to call under GC. */ +__private_extern__ void HFGlyphTreeFree(struct HFGlyphTrie_t * trie); + + diff --git a/bsnes/gb/HexFiend/HFGlyphTrie.m b/bsnes/gb/HexFiend/HFGlyphTrie.m new file mode 100644 index 00000000..db94782a --- /dev/null +++ b/bsnes/gb/HexFiend/HFGlyphTrie.m @@ -0,0 +1,93 @@ +#import "HFGlyphTrie.h" +#import + +/* If branchingDepth is 1, then this is a leaf and there's nothing to free (a parent frees its children). If branchingDepth is 2, then this is a branch whose children are leaves, so we have to free the leaves but we do not recurse. If branchingDepth is greater than 2, we do have to recurse. */ +static void freeTrie(struct HFGlyphTrieBranch_t *branch, uint8_t branchingDepth) { + HFASSERT(branchingDepth >= 1); + NSUInteger i; + if (branchingDepth > 2) { + /* Recurse */ + for (i=0; i < kHFGlyphTrieBranchCount; i++) { + if (branch->children[i]) { + freeTrie(branch->children[i], branchingDepth - 1); + } + } + } + if (branchingDepth > 1) { + /* Free our children */ + for (i=0; i < kHFGlyphTrieBranchCount; i++) { + free(branch->children[i]); + } + } +} + +static void insertTrie(void *node, uint8_t branchingDepth, NSUInteger key, struct HFGlyph_t value) { + HFASSERT(node != NULL); + HFASSERT(branchingDepth >= 1); + if (branchingDepth == 1) { + /* Leaf */ + HFASSERT(key < kHFGlyphTrieBranchCount); + ((struct HFGlyphTrieLeaf_t *)node)->glyphs[key] = value; + } else { + /* Branch */ + struct HFGlyphTrieBranch_t *branch = node; + NSUInteger keySlice = key & ((1 << kHFGlyphTrieBranchFactor) - 1), keyRemainder = key >> kHFGlyphTrieBranchFactor; + __strong void *child = branch->children[keySlice]; + if (child == NULL) { + if (branchingDepth == 2) { + child = calloc(1, sizeof(struct HFGlyphTrieLeaf_t)); + } else { + child = calloc(1, sizeof(struct HFGlyphTrieBranch_t)); + } + /* We just zeroed out a block of memory and we are about to write its address somewhere where another thread could read it, so we need a memory barrier. */ + OSMemoryBarrier(); + branch->children[keySlice] = child; + } + insertTrie(child, branchingDepth - 1, keyRemainder, value); + } +} + +static struct HFGlyph_t getTrie(const void *node, uint8_t branchingDepth, NSUInteger key) { + HFASSERT(node != NULL); + HFASSERT(branchingDepth >= 1); + if (branchingDepth == 1) { + /* Leaf */ + HFASSERT(key < kHFGlyphTrieBranchCount); + return ((const struct HFGlyphTrieLeaf_t *)node)->glyphs[key]; + } else { + /* Branch */ + const struct HFGlyphTrieBranch_t *branch = node; + NSUInteger keySlice = key & ((1 << kHFGlyphTrieBranchFactor) - 1), keyRemainder = key >> kHFGlyphTrieBranchFactor; + if (branch->children[keySlice] == NULL) { + /* Not found */ + return (struct HFGlyph_t){0, 0}; + } else { + /* This dereference requires a data dependency barrier */ + return getTrie(branch->children[keySlice], branchingDepth - 1, keyRemainder); + } + } +} + +void HFGlyphTrieInsert(struct HFGlyphTrie_t *trie, NSUInteger key, struct HFGlyph_t value) { + insertTrie(&trie->root, trie->branchingDepth, key, value); +} + +struct HFGlyph_t HFGlyphTrieGet(const struct HFGlyphTrie_t *trie, NSUInteger key) { + struct HFGlyph_t result = getTrie(&trie->root, trie->branchingDepth, key); + return result; +} + +void HFGlyphTrieInitialize(struct HFGlyphTrie_t *trie, uint8_t keySize) { + /* If the branch factor is 4 (bits) and the key size is 2 bytes = 16 bits, initialize branching depth to 16/4 = 4 */ + uint8_t keyBits = keySize * CHAR_BIT; + HFASSERT(keyBits % kHFGlyphTrieBranchFactor == 0); + trie->branchingDepth = keyBits / kHFGlyphTrieBranchFactor; + memset(&trie->root, 0, sizeof(trie->root)); +} + +void HFGlyphTreeFree(struct HFGlyphTrie_t * trie) { + /* Don't try to free under GC. And don't free if it's never been initialized. */ + if (trie->branchingDepth > 0) { + freeTrie(&trie->root, trie->branchingDepth); + } +} diff --git a/bsnes/gb/HexFiend/HFHexTextRepresenter.h b/bsnes/gb/HexFiend/HFHexTextRepresenter.h new file mode 100644 index 00000000..ecbb1a0b --- /dev/null +++ b/bsnes/gb/HexFiend/HFHexTextRepresenter.h @@ -0,0 +1,21 @@ +// +// HFHexTextRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFHexTextRepresenter + + @brief HFHexTextRepresenter is an HFRepresenter responsible for showing data in hexadecimal form. + + HFHexTextRepresenter is an HFRepresenter responsible for showing data in hexadecimal form. It has no methods except those inherited from HFTextRepresenter. +*/ +@interface HFHexTextRepresenter : HFTextRepresenter { + unsigned long long omittedNybbleLocation; + unsigned char unpartneredLastNybble; +} + +@end diff --git a/bsnes/gb/HexFiend/HFHexTextRepresenter.m b/bsnes/gb/HexFiend/HFHexTextRepresenter.m new file mode 100644 index 00000000..f98382b6 --- /dev/null +++ b/bsnes/gb/HexFiend/HFHexTextRepresenter.m @@ -0,0 +1,203 @@ +// +// HFHexTextRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +@interface HFHexPasteboardOwner : HFPasteboardOwner { + NSUInteger _bytesPerColumn; +} +@property (nonatomic) NSUInteger bytesPerColumn; +@end + +static inline unsigned char hex2char(NSUInteger c) { + HFASSERT(c < 16); + return "0123456789ABCDEF"[c]; +} + +@implementation HFHexPasteboardOwner + +@synthesize bytesPerColumn = _bytesPerColumn; + +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength { + if(!dataLength) return 0; + // -1 because no trailing space for an exact multiple. + unsigned long long spaces = _bytesPerColumn ? (dataLength-1)/_bytesPerColumn : 0; + if ((ULLONG_MAX - spaces)/2 <= dataLength) return ULLONG_MAX; + else return dataLength*2 + spaces; +} + +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker { + HFASSERT([type isEqual:NSStringPboardType]); + if(length == 0) { + [pboard setString:@"" forType:type]; + return; + } + HFByteArray *byteArray = [self byteArray]; + HFASSERT(length <= NSUIntegerMax); + NSUInteger dataLength = ll2l(length); + NSUInteger stringLength = ll2l([self stringLengthForDataLength:length]); + HFASSERT(stringLength < ULLONG_MAX); + NSUInteger offset = 0, stringOffset = 0, remaining = dataLength; + unsigned char * restrict const stringBuffer = check_malloc(stringLength); + while (remaining > 0) { + unsigned char dataBuffer[64 * 1024]; + NSUInteger amountToCopy = MIN(sizeof dataBuffer, remaining); + NSUInteger bound = offset + amountToCopy - 1; + [byteArray copyBytes:dataBuffer range:HFRangeMake(offset, amountToCopy)]; + + if(_bytesPerColumn > 0 && offset > 0) { // ensure offset > 0 to skip adding a leading space + NSUInteger left = _bytesPerColumn - (offset % _bytesPerColumn); + if(left != _bytesPerColumn) { + while(left-- > 0 && offset <= bound) { + unsigned char c = dataBuffer[offset++]; + stringBuffer[stringOffset] = hex2char(c >> 4); + stringBuffer[stringOffset + 1] = hex2char(c & 0xF); + stringOffset += 2; + } + } + if(offset <= bound) + stringBuffer[stringOffset++] = ' '; + } + + if(_bytesPerColumn > 0) while(offset+_bytesPerColumn <= bound) { + for(NSUInteger j = 0; j < _bytesPerColumn; j++) { + unsigned char c = dataBuffer[offset++]; + stringBuffer[stringOffset] = hex2char(c >> 4); + stringBuffer[stringOffset + 1] = hex2char(c & 0xF); + stringOffset += 2; + } + stringBuffer[stringOffset++] = ' '; + } + + while (offset <= bound) { + unsigned char c = dataBuffer[offset++]; + stringBuffer[stringOffset] = hex2char(c >> 4); + stringBuffer[stringOffset + 1] = hex2char(c & 0xF); + stringOffset += 2; + } + + remaining -= amountToCopy; + } + + NSString *string = [[NSString alloc] initWithBytesNoCopy:stringBuffer length:stringLength encoding:NSASCIIStringEncoding freeWhenDone:YES]; + [pboard setString:string forType:type]; + [string release]; +} + +@end + +@implementation HFHexTextRepresenter + +/* No extra NSCoder support needed */ + +- (Class)_textViewClass { + return [HFRepresenterHexTextView class]; +} + +- (void)initializeView { + [super initializeView]; + [[self view] setBytesBetweenVerticalGuides:4]; + unpartneredLastNybble = UCHAR_MAX; + omittedNybbleLocation = ULLONG_MAX; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(0, 0); +} + +- (void)_clearOmittedNybble { + unpartneredLastNybble = UCHAR_MAX; + omittedNybbleLocation = ULLONG_MAX; +} + +- (BOOL)_insertionShouldDeleteLastNybble { + /* Either both the omittedNybbleLocation and unpartneredLastNybble are invalid (set to their respective maxima), or neither are */ + HFASSERT((omittedNybbleLocation == ULLONG_MAX) == (unpartneredLastNybble == UCHAR_MAX)); + /* We should delete the last nybble if our omittedNybbleLocation is the point where we would insert */ + BOOL result = NO; + if (omittedNybbleLocation != ULLONG_MAX) { + HFController *controller = [self controller]; + NSArray *selectedRanges = [controller selectedContentsRanges]; + if ([selectedRanges count] == 1) { + HFRange selectedRange = [selectedRanges[0] HFRange]; + result = (selectedRange.length == 0 && selectedRange.location > 0 && selectedRange.location - 1 == omittedNybbleLocation); + } + } + return result; +} + +- (BOOL)_canInsertText:(NSString *)text { + REQUIRE_NOT_NULL(text); + NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEFabcdef"]; + return [text rangeOfCharacterFromSet:characterSet].location != NSNotFound; +} + +- (void)insertText:(NSString *)text { + REQUIRE_NOT_NULL(text); + if (! [self _canInsertText:text]) { + /* The user typed invalid data, and we can ignore it */ + return; + } + + BOOL shouldReplacePriorByte = [self _insertionShouldDeleteLastNybble]; + if (shouldReplacePriorByte) { + HFASSERT(unpartneredLastNybble < 16); + /* Prepend unpartneredLastNybble as a nybble */ + text = [NSString stringWithFormat:@"%1X%@", unpartneredLastNybble, text]; + } + BOOL isMissingLastNybble; + NSData *data = HFDataFromHexString(text, &isMissingLastNybble); + HFASSERT([data length] > 0); + HFASSERT(shouldReplacePriorByte != isMissingLastNybble); + HFController *controller = [self controller]; + BOOL success = [controller insertData:data replacingPreviousBytes: (shouldReplacePriorByte ? 1 : 0) allowUndoCoalescing:YES]; + if (isMissingLastNybble && success) { + HFASSERT([data length] > 0); + HFASSERT(unpartneredLastNybble == UCHAR_MAX); + [data getBytes:&unpartneredLastNybble range:NSMakeRange([data length] - 1, 1)]; + NSArray *selectedRanges = [controller selectedContentsRanges]; + HFASSERT([selectedRanges count] >= 1); + HFRange selectedRange = [selectedRanges[0] HFRange]; + HFASSERT(selectedRange.location > 0); + omittedNybbleLocation = HFSubtract(selectedRange.location, 1); + } + else { + [self _clearOmittedNybble]; + } +} + +- (NSData *)dataFromPasteboardString:(NSString *)string { + REQUIRE_NOT_NULL(string); + return HFDataFromHexString(string, NULL); +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & HFControllerHideNullBytes) { + [[self view] setHidesNullBytes:[[self controller] shouldHideNullBytes]]; + } + [super controllerDidChange:bits]; + if (bits & (HFControllerContentValue | HFControllerContentLength | HFControllerSelectedRanges)) { + [self _clearOmittedNybble]; + } +} + +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + HFByteArray *selection = [[self controller] byteArrayForSelectedContentsRanges]; + HFASSERT(selection != NULL); + if ([selection length] == 0) { + NSBeep(); + } else { + HFHexPasteboardOwner *owner = [HFHexPasteboardOwner ownPasteboard:pb forByteArray:selection withTypes:@[HFPrivateByteArrayPboardType, NSStringPboardType]]; + [owner setBytesPerLine:[self bytesPerLine]]; + owner.bytesPerColumn = self.bytesPerColumn; + } +} + +@end diff --git a/bsnes/gb/HexFiend/HFHexTextView.h b/bsnes/gb/HexFiend/HFHexTextView.h new file mode 100644 index 00000000..3222b65e --- /dev/null +++ b/bsnes/gb/HexFiend/HFHexTextView.h @@ -0,0 +1,15 @@ +// +// HFHexTextView.h +// HexFiend_2 +// +// Copyright 2007 __MyCompanyName__. All rights reserved. +// + +#import + + +@interface HFHexTextView : NSTextView { + +} + +@end diff --git a/bsnes/gb/HexFiend/HFHexTextView.m b/bsnes/gb/HexFiend/HFHexTextView.m new file mode 100644 index 00000000..9e6ae47b --- /dev/null +++ b/bsnes/gb/HexFiend/HFHexTextView.m @@ -0,0 +1,13 @@ +// +// HFHexTextView.m +// HexFiend_2 +// +// Copyright 2007 __MyCompanyName__. All rights reserved. +// + +#import "HFHexTextView.h" + + +@implementation HFHexTextView + +@end diff --git a/bsnes/gb/HexFiend/HFLayoutRepresenter.h b/bsnes/gb/HexFiend/HFLayoutRepresenter.h new file mode 100644 index 00000000..a493304b --- /dev/null +++ b/bsnes/gb/HexFiend/HFLayoutRepresenter.h @@ -0,0 +1,80 @@ +// +// HFLayoutRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFLayoutRepresenter + @brief An HFRepresenter responsible for arranging the views of other HFRepresenters attached to the same HFController. + + HFLayoutRepresenter is an HFRepresenter that manages the views of other HFRepresenters. It arranges their views in its own view, mediating between them to determine their position and size, as well as global properties such as bytes per line. + + HFLayoutRepresenter has an array of representers attached to it. When you add an HFRepresenter to this array, HFLayoutRepresenter will add the view of the representer as a subview of its own view. + + \b Layout + + HFLayoutRepresenter is capable of arranging the views of other HFRepresenters to fit within the bounds of its view. The layout process depends on three things: + + -# The \c frame and \c autoresizingMask of the representers' views. + -# The \c minimumViewWidthForBytesPerLine: method, which determines the largest number of bytes per line that the representer can display for a given view width. + -# The representer's \c layoutPosition. This is an NSPoint, but it is not used geometrically. Instead, the relative values of the X and Y coordinates of the \c layoutPosition determine the relative positioning of the views, as described below. + + Thus, to have your subclass of HFRepresenter participate in the HFLayoutRepresenter system, override \c defaultLayoutPosition: to control its positioning, and possibly \\c minimumViewWidthForBytesPerLine: if your representer requires a certain width to display some bytes per line. Then ensure your view has its autoresizing mask set properly, and if its frame is fixed size, ensure that its frame is correct as well. + + The layout process, in detail, is: + + -# The views are sorted vertically by the Y component of their representers' \c layoutPosition into "slices." Smaller values appear towards the bottom of the layout view. There is no space between slices. + -# Views with equal Y components are sorted horizontally by the X component of their representers' \c layoutPosition, with smaller values appearing on the left. + -# The height of each slice is determined by the tallest view within it, excluding views that have \c NSViewHeightSizable set. If there is any leftover vertical space, it is distributed equally among all slices with at least one view with \c NSViewHeightSizable set. + -# If the layout representer is not set to maximize the bytes per line (BPL), then the BPL from the HFController is used. Otherwise: + -# Each representer is queried for its \c minimumViewWidthForBytesPerLine: + -# The largest BPL allowing each row to fit within the layout width is determined via a binary search. + -# The BPL is rounded down to a multiple of the bytes per column (if non-zero). + -# The BPL is then set on the controller. + -# For each row, each view is assigned its minimum view width for the BPL. + -# If there is any horizontal space left over, it is divided evenly between all views in that slice that have \c NSViewWidthSizable set in their autoresizing mask. + +*/ +@interface HFLayoutRepresenter : HFRepresenter { + NSMutableArray *representers; + BOOL maximizesBytesPerLine; +} + +/*! @name Managed representers + Managing the list of representers laid out by the receiver +*/ +//@{ +/// Return the array of representers managed by the receiver. */ +@property (readonly, copy) NSArray *representers; + +/*! Adds a new representer to the receiver, triggering relayout. */ +- (void)addRepresenter:(HFRepresenter *)representer; + +/*! Removes a representer to the receiver (which must be present in the receiver's array of representers), triggering relayout. */ +- (void)removeRepresenter:(HFRepresenter *)representer; +//@} + +/*! When enabled, the receiver will attempt to maximize the bytes per line so as to consume as much as possible of the bounds rect. If this is YES, then upon relayout, the receiver will recalculate the maximum number of bytes per line that can fit in its boundsRectForLayout. If this is NO, then the receiver will not change the bytes per line. */ +@property (nonatomic) BOOL maximizesBytesPerLine; + +/*! @name Layout + Methods to get information about layout, and to explicitly trigger it. +*/ +//@{ +/*! Returns the smallest width that produces the same layout (and, if maximizes bytesPerLine, the same bytes per line) as the proposed width. */ +- (CGFloat)minimumViewWidthForLayoutInProposedWidth:(CGFloat)proposedWidth; + +/*! Returns the maximum bytes per line that can fit in the proposed width (ignoring maximizesBytesPerLine). This is always a multiple of the bytesPerColumn, and always at least bytesPerColumn. */ +- (NSUInteger)maximumBytesPerLineForLayoutInProposedWidth:(CGFloat)proposedWidth; + +/*! Returns the smallest width that can support the given bytes per line. */ +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine; + +/*! Relayouts are triggered when representers are added and removed, or when the view is resized. You may call this explicitly to trigger a relayout. */ +- (void)performLayout; +//@} + +@end diff --git a/bsnes/gb/HexFiend/HFLayoutRepresenter.m b/bsnes/gb/HexFiend/HFLayoutRepresenter.m new file mode 100644 index 00000000..c36c0ea1 --- /dev/null +++ b/bsnes/gb/HexFiend/HFLayoutRepresenter.m @@ -0,0 +1,361 @@ +// +// HFRepresenterLayoutView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +@interface HFRepresenterLayoutViewInfo : NSObject { +@public + HFRepresenter *rep; + NSView *view; + NSPoint layoutPosition; + NSRect frame; + NSUInteger autoresizingMask; +} + +@end + +@implementation HFRepresenterLayoutViewInfo + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ : %@>", view, NSStringFromRect(frame)]; +} + +@end + +@implementation HFLayoutRepresenter + +static NSInteger sortByLayoutPosition(id a, id b, void *self) { + USE(self); + NSPoint pointA = [a layoutPosition]; + NSPoint pointB = [b layoutPosition]; + if (pointA.y < pointB.y) return -1; + else if (pointA.y > pointB.y) return 1; + else if (pointA.x < pointB.x) return -1; + else if (pointA.x > pointB.x) return 1; + else return 0; +} + +- (NSArray *)arraysOfLayoutInfos { + if (! representers) return nil; + + NSMutableArray *result = [NSMutableArray array]; + NSArray *reps = [representers sortedArrayUsingFunction:sortByLayoutPosition context:self]; + NSMutableArray *currentReps = [NSMutableArray array]; + CGFloat currentRepY = - CGFLOAT_MAX; + FOREACH(HFRepresenter*, rep, reps) { + HFRepresenterLayoutViewInfo *info = [[HFRepresenterLayoutViewInfo alloc] init]; + info->rep = rep; + info->view = [rep view]; + info->frame = [info->view frame]; + info->layoutPosition = [rep layoutPosition]; + info->autoresizingMask = [info->view autoresizingMask]; + if (info->layoutPosition.y != currentRepY && [currentReps count] > 0) { + [result addObject:[[currentReps copy] autorelease]]; + [currentReps removeAllObjects]; + } + currentRepY = info->layoutPosition.y; + [currentReps addObject:info]; + [info release]; + } + if ([currentReps count]) [result addObject:[[currentReps copy] autorelease]]; + return result; +} + +- (NSRect)boundsRectForLayout { + NSRect result = [[self view] bounds]; + /* Sometimes when we are not yet in a window, we get wonky bounds, so be paranoid. */ + if (result.size.width < 0 || result.size.height < 0) result = NSZeroRect; + return result; +} + +- (CGFloat)_computeMinHeightForLayoutInfos:(NSArray *)infos { + CGFloat result = 0; + HFASSERT(infos != NULL); + HFASSERT([infos count] > 0); + FOREACH(HFRepresenterLayoutViewInfo *, info, infos) { + if (! (info->autoresizingMask & NSViewHeightSizable)) result = MAX(result, NSHeight([info->view frame])); + } + return result; +} + +- (void)_applyYLocation:(CGFloat)yLocation andMinHeight:(CGFloat)height toInfos:(NSArray *)layoutInfos { + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + info->frame.origin.y = yLocation; + if (info->autoresizingMask & NSViewHeightSizable) info->frame.size.height = height; + } +} + +- (void)_layoutInfosHorizontally:(NSArray *)infos inRect:(NSRect)layoutRect withBytesPerLine:(NSUInteger)bytesPerLine { + CGFloat nextX = NSMinX(layoutRect); + NSUInteger numHorizontallyResizable = 0; + FOREACH(HFRepresenterLayoutViewInfo *, info, infos) { + CGFloat minWidth = [info->rep minimumViewWidthForBytesPerLine:bytesPerLine]; + info->frame.origin.x = nextX; + info->frame.size.width = minWidth; + nextX += minWidth; + numHorizontallyResizable += !! (info->autoresizingMask & NSViewWidthSizable); + } + + CGFloat remainingWidth = NSMaxX(layoutRect) - nextX; + if (numHorizontallyResizable > 0 && remainingWidth > 0) { + NSView *view = [self view]; + CGFloat remainingPixels = [view convertSize:NSMakeSize(remainingWidth, 0) toView:nil].width; + HFASSERT(remainingPixels > 0); + CGFloat pixelsPerView = HFFloor(HFFloor(remainingPixels) / (CGFloat)numHorizontallyResizable); + if (pixelsPerView > 0) { + CGFloat pointsPerView = [view convertSize:NSMakeSize(pixelsPerView, 0) fromView:nil].width; + CGFloat pointsAdded = 0; + FOREACH(HFRepresenterLayoutViewInfo *, info, infos) { + info->frame.origin.x += pointsAdded; + if (info->autoresizingMask & NSViewWidthSizable) { + info->frame.size.width += pointsPerView; + pointsAdded += pointsPerView; + } + } + } + } +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + CGFloat result = 0; + NSArray *arraysOfLayoutInfos = [self arraysOfLayoutInfos]; + + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + CGFloat minWidthForRow = 0; + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + minWidthForRow += [info->rep minimumViewWidthForBytesPerLine:bytesPerLine]; + } + result = MAX(result, minWidthForRow); + } + return result; +} + +- (NSUInteger)_computeBytesPerLineForArraysOfLayoutInfos:(NSArray *)arraysOfLayoutInfos forLayoutInRect:(NSRect)layoutRect { + /* The granularity is our own granularity (probably 1), LCMed with the granularities of all other representers */ + NSUInteger granularity = [self byteGranularity]; + FOREACH(HFRepresenter *, representer, representers) { + granularity = HFLeastCommonMultiple(granularity, [representer byteGranularity]); + } + HFASSERT(granularity >= 1); + + NSUInteger newNumGranules = (NSUIntegerMax - 1) / granularity; + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + NSUInteger maxKnownGood = 0, minKnownBad = newNumGranules + 1; + while (maxKnownGood + 1 < minKnownBad) { + CGFloat requiredSpace = 0; + NSUInteger proposedNumGranules = maxKnownGood + (minKnownBad - maxKnownGood)/2; + NSUInteger proposedBytesPerLine = proposedNumGranules * granularity; + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + requiredSpace += [info->rep minimumViewWidthForBytesPerLine:proposedBytesPerLine]; + if (requiredSpace > NSWidth(layoutRect)) break; + } + if (requiredSpace > NSWidth(layoutRect)) minKnownBad = proposedNumGranules; + else maxKnownGood = proposedNumGranules; + } + newNumGranules = maxKnownGood; + } + return MAX(1u, newNumGranules) * granularity; +} + +- (BOOL)_anyLayoutInfoIsVerticallyResizable:(NSArray *)vals { + HFASSERT(vals != NULL); + FOREACH(HFRepresenterLayoutViewInfo *, info, vals) { + if (info->autoresizingMask & NSViewHeightSizable) return YES; + } + return NO; +} + +- (BOOL)_addVerticalHeight:(CGFloat)heightPoints andOffset:(CGFloat)offsetPoints toLayoutInfos:(NSArray *)layoutInfos { + BOOL isVerticallyResizable = [self _anyLayoutInfoIsVerticallyResizable:layoutInfos]; + CGFloat totalHeight = [self _computeMinHeightForLayoutInfos:layoutInfos] + heightPoints; + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfos) { + info->frame.origin.y += offsetPoints; + if (isVerticallyResizable) { + if (info->autoresizingMask & NSViewHeightSizable) { + info->frame.size.height = totalHeight; + } + else { + CGFloat diff = totalHeight - info->frame.size.height; + HFASSERT(diff >= 0); + info->frame.origin.y += HFFloor(diff); + } + } + } + return isVerticallyResizable; +} + +- (void)_distributeVerticalSpace:(CGFloat)space toArraysOfLayoutInfos:(NSArray *)arraysOfLayoutInfos { + HFASSERT(space >= 0); + HFASSERT(arraysOfLayoutInfos != NULL); + + NSUInteger consumers = 0; + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + if ([self _anyLayoutInfoIsVerticallyResizable:layoutInfos]) consumers++; + } + if (consumers > 0) { + NSView *view = [self view]; + CGFloat availablePixels = [view convertSize:NSMakeSize(0, space) toView:nil].height; + HFASSERT(availablePixels > 0); + CGFloat pixelsPerView = HFFloor(HFFloor(availablePixels) / (CGFloat)consumers); + CGFloat pointsPerView = [view convertSize:NSMakeSize(0, pixelsPerView) fromView:nil].height; + CGFloat yOffset = 0; + if (pointsPerView > 0) { + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + if ([self _addVerticalHeight:pointsPerView andOffset:yOffset toLayoutInfos:layoutInfos]) { + yOffset += pointsPerView; + } + } + } + } +} + +- (void)performLayout { + HFController *controller = [self controller]; + if (! controller) return; + if (! representers) return; + + NSArray *arraysOfLayoutInfos = [self arraysOfLayoutInfos]; + if (! [arraysOfLayoutInfos count]) return; + + NSUInteger transaction = [controller beginPropertyChangeTransaction]; + + NSRect layoutRect = [self boundsRectForLayout]; + + NSUInteger bytesPerLine; + if (maximizesBytesPerLine) bytesPerLine = [self _computeBytesPerLineForArraysOfLayoutInfos:arraysOfLayoutInfos forLayoutInRect:layoutRect]; + else bytesPerLine = [controller bytesPerLine]; + + CGFloat yPosition = NSMinY(layoutRect); + FOREACH(NSArray *, layoutInfos, arraysOfLayoutInfos) { + HFASSERT([layoutInfos count] > 0); + CGFloat minHeight = [self _computeMinHeightForLayoutInfos:layoutInfos]; + [self _applyYLocation:yPosition andMinHeight:minHeight toInfos:layoutInfos]; + yPosition += minHeight; + [self _layoutInfosHorizontally:layoutInfos inRect:layoutRect withBytesPerLine:bytesPerLine]; + } + + CGFloat remainingVerticalSpace = NSMaxY(layoutRect) - yPosition; + if (remainingVerticalSpace > 0) { + [self _distributeVerticalSpace:remainingVerticalSpace toArraysOfLayoutInfos:arraysOfLayoutInfos]; + } + + FOREACH(NSArray *, layoutInfoArray, arraysOfLayoutInfos) { + FOREACH(HFRepresenterLayoutViewInfo *, info, layoutInfoArray) { + [info->view setFrame:info->frame]; + } + } + + [controller endPropertyChangeTransaction:transaction]; +} + +- (NSArray *)representers { + return representers ? [[representers copy] autorelease] : @[]; +} + +- (instancetype)init { + self = [super init]; + maximizesBytesPerLine = YES; + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self view]]; + [representers release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeObject:representers forKey:@"HFRepresenters"]; + [coder encodeBool:maximizesBytesPerLine forKey:@"HFMaximizesBytesPerLine"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + representers = [[coder decodeObjectForKey:@"HFRepresenters"] retain]; + maximizesBytesPerLine = [coder decodeBoolForKey:@"HFMaximizesBytesPerLine"]; + NSView *view = [self view]; + [view setPostsFrameChangedNotifications:YES]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameChanged:) name:NSViewFrameDidChangeNotification object:view]; + return self; +} + +- (void)addRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + if (! representers) representers = [[NSMutableArray alloc] init]; + HFASSERT([representers indexOfObjectIdenticalTo:representer] == NSNotFound); + [representers addObject:representer]; + HFASSERT([[representer view] superview] != [self view]); + [[self view] addSubview:[representer view]]; + [self performLayout]; +} + +- (void)removeRepresenter:(HFRepresenter *)representer { + REQUIRE_NOT_NULL(representer); + HFASSERT([representers indexOfObjectIdenticalTo:representer] != NSNotFound); + NSView *view = [representer view]; + HFASSERT([view superview] == [self view]); + [view removeFromSuperview]; + [representers removeObjectIdenticalTo:representer]; + [self performLayout]; +} + +- (void)frameChanged:(NSNotification *)note { + USE(note); + [self performLayout]; +} + +- (void)initializeView { + NSView *view = [self view]; + [view setPostsFrameChangedNotifications:YES]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(frameChanged:) name:NSViewFrameDidChangeNotification object:view]; +} + +- (NSView *)createView { + return [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]; +} + +- (void)setMaximizesBytesPerLine:(BOOL)val { + maximizesBytesPerLine = val; +} + +- (BOOL)maximizesBytesPerLine { + return maximizesBytesPerLine; +} + +- (NSUInteger)maximumBytesPerLineForLayoutInProposedWidth:(CGFloat)proposedWidth { + NSArray *arraysOfLayoutInfos = [self arraysOfLayoutInfos]; + if (! [arraysOfLayoutInfos count]) return 0; + + NSRect layoutRect = [self boundsRectForLayout]; + layoutRect.size.width = proposedWidth; + + NSUInteger bytesPerLine = [self _computeBytesPerLineForArraysOfLayoutInfos:arraysOfLayoutInfos forLayoutInRect:layoutRect]; + return bytesPerLine; +} + +- (CGFloat)minimumViewWidthForLayoutInProposedWidth:(CGFloat)proposedWidth { + NSUInteger bytesPerLine; + if ([self maximizesBytesPerLine]) { + bytesPerLine = [self maximumBytesPerLineForLayoutInProposedWidth:proposedWidth]; + } else { + bytesPerLine = [[self controller] bytesPerLine]; + } + CGFloat newWidth = [self minimumViewWidthForBytesPerLine:bytesPerLine]; + return newWidth; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + [super controllerDidChange:bits]; + if (bits & (HFControllerViewSizeRatios | HFControllerBytesPerColumn | HFControllerByteGranularity)) { + [self performLayout]; + } +} + +@end diff --git a/bsnes/gb/HexFiend/HFLineCountingRepresenter.h b/bsnes/gb/HexFiend/HFLineCountingRepresenter.h new file mode 100644 index 00000000..b711e4ee --- /dev/null +++ b/bsnes/gb/HexFiend/HFLineCountingRepresenter.h @@ -0,0 +1,67 @@ +// +// HFLineCountingRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @enum HFLineNumberFormat + HFLineNumberFormat is a simple enum used to determine whether line numbers are in decimal or hexadecimal format. +*/ +typedef NS_ENUM(NSUInteger, HFLineNumberFormat) { + HFLineNumberFormatDecimal, //!< Decimal line numbers + HFLineNumberFormatHexadecimal, //!< Hexadecimal line numbers + HFLineNumberFormatMAXIMUM //!< One more than the maximum valid line number format, so that line number formats can be cycled through easily +}; + +/*! @class HFLineCountingRepresenter + @brief The HFRepresenter used to show the "line number gutter." + + HFLineCountingRepresenter is the HFRepresenter used to show the "line number gutter." HFLineCountingRepresenter makes space for a certain number of digits. +*/ +@interface HFLineCountingRepresenter : HFRepresenter { + CGFloat lineHeight; + NSUInteger digitsToRepresentContentsLength; + NSUInteger minimumDigitCount; + HFLineNumberFormat lineNumberFormat; + NSInteger interiorShadowEdge; + CGFloat preferredWidth; + CGFloat digitAdvance; +} + +/// The minimum digit count. The receiver will always ensure it is big enough to display at least the minimum digit count. The default is 2. +@property (nonatomic) NSUInteger minimumDigitCount; + +/// The number of digits we are making space for. +@property (readonly) NSUInteger digitCount; + +/// The current width that the HFRepresenter prefers to be laid out with. +@property (readonly) CGFloat preferredWidth; + +/// The line number format. +@property (nonatomic) HFLineNumberFormat lineNumberFormat; + +/// Switches to the next line number format. This is called from the view. +- (void)cycleLineNumberFormat; + +/// The edge (as an NSRectEdge) on which the view draws an interior shadow. -1 means no edge. +@property (nonatomic) NSInteger interiorShadowEdge; + +/// The border color used at the edges specified by -borderedEdges. +@property (nonatomic, copy) NSColor *borderColor; + +/*! The edges on which borders are drawn. The edge returned by interiorShadowEdge always has a border drawn. The edges are specified by a bitwise or of 1 left shifted by the NSRectEdge values. For example, to draw a border on the min x and max y edges use: (1 << NSMinXEdge) | (1 << NSMaxYEdge). 0 (or -1) specfies no edges. */ +@property (nonatomic) NSInteger borderedEdges; + +/// The background color +@property (nonatomic, copy) NSColor *backgroundColor; + +@property NSUInteger valueOffset; + +@end + +/*! Notification posted when the HFLineCountingRepresenter's width has changed because the number of digits it wants to show has increased or decreased. The object is the HFLineCountingRepresenter; there is no user info. +*/ +extern NSString *const HFLineCountingRepresenterMinimumViewWidthChanged; diff --git a/bsnes/gb/HexFiend/HFLineCountingRepresenter.m b/bsnes/gb/HexFiend/HFLineCountingRepresenter.m new file mode 100644 index 00000000..4a81fc83 --- /dev/null +++ b/bsnes/gb/HexFiend/HFLineCountingRepresenter.m @@ -0,0 +1,250 @@ +// +// HFLineCountingRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +NSString *const HFLineCountingRepresenterMinimumViewWidthChanged = @"HFLineCountingRepresenterMinimumViewWidthChanged"; + +/* Returns the maximum advance in points for a hexadecimal digit for the given font (interpreted as a screen font) */ +static CGFloat maximumDigitAdvanceForFont(NSFont *font) { + REQUIRE_NOT_NULL(font); + font = [font screenFont]; + CGFloat maxDigitAdvance = 0; + NSDictionary *attributesDictionary = [[NSDictionary alloc] initWithObjectsAndKeys:font, NSFontAttributeName, nil]; + NSTextStorage *storage = [[NSTextStorage alloc] init]; + NSLayoutManager *manager = [[NSLayoutManager alloc] init]; + [storage setFont:font]; + [storage addLayoutManager:manager]; + + NSSize advancements[16] = {}; + NSGlyph glyphs[16]; + + /* Generate a glyph for every hex digit */ + for (NSUInteger i=0; i < 16; i++) { + char c = "0123456789ABCDEF"[i]; + NSString *string = [[NSString alloc] initWithBytes:&c length:1 encoding:NSASCIIStringEncoding]; + [storage replaceCharactersInRange:NSMakeRange(0, (i ? 1 : 0)) withString:string]; + [string release]; + glyphs[i] = [manager glyphAtIndex:0 isValidIndex:NULL]; + HFASSERT(glyphs[i] != NSNullGlyph); + } + + /* Get the advancements of each of those glyphs */ + [font getAdvancements:advancements forGlyphs:glyphs count:sizeof glyphs / sizeof *glyphs]; + + [manager release]; + [attributesDictionary release]; + [storage release]; + + /* Find the widest digit */ + for (NSUInteger i=0; i < sizeof glyphs / sizeof *glyphs; i++) { + maxDigitAdvance = HFMax(maxDigitAdvance, advancements[i].width); + } + return maxDigitAdvance; +} + +@implementation HFLineCountingRepresenter + +- (instancetype)init { + if ((self = [super init])) { + minimumDigitCount = 2; + digitsToRepresentContentsLength = minimumDigitCount; + interiorShadowEdge = NSMaxXEdge; + + _borderedEdges = (1 << NSMaxXEdge); + _borderColor = [[NSColor controlShadowColor] retain]; + _backgroundColor = [[NSColor windowBackgroundColor] retain]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeDouble:lineHeight forKey:@"HFLineHeight"]; + [coder encodeInt64:minimumDigitCount forKey:@"HFMinimumDigitCount"]; + [coder encodeInt64:lineNumberFormat forKey:@"HFLineNumberFormat"]; + [coder encodeObject:self.backgroundColor forKey:@"HFBackgroundColor"]; + [coder encodeObject:self.borderColor forKey:@"HFBorderColor"]; + [coder encodeInt64:self.borderedEdges forKey:@"HFBorderedEdges"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + lineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFLineHeight"]; + minimumDigitCount = (NSUInteger)[coder decodeInt64ForKey:@"HFMinimumDigitCount"]; + lineNumberFormat = (HFLineNumberFormat)[coder decodeInt64ForKey:@"HFLineNumberFormat"]; + + _borderedEdges = [coder decodeObjectForKey:@"HFBorderedEdges"] ? (NSInteger)[coder decodeInt64ForKey:@"HFBorderedEdges"] : 0; + _borderColor = [[NSColor controlShadowColor] retain]; + _backgroundColor = [[NSColor windowBackgroundColor] retain]; + + return self; +} + +- (void)dealloc { + [_borderColor release]; + [_backgroundColor release]; + [super dealloc]; +} + +- (NSView *)createView { + HFLineCountingView *result = [[HFLineCountingView alloc] initWithFrame:NSMakeRect(0, 0, 60, 10)]; + [result setRepresenter:self]; + [result setAutoresizingMask:NSViewHeightSizable]; + return result; +} + +- (void)postMinimumViewWidthChangedNotification { + [[NSNotificationCenter defaultCenter] postNotificationName:HFLineCountingRepresenterMinimumViewWidthChanged object:self]; +} + +- (void)updateDigitAdvanceWithFont:(NSFont *)font { + CGFloat newDigitAdvance = maximumDigitAdvanceForFont(font); + if (digitAdvance != newDigitAdvance) { + digitAdvance = newDigitAdvance; + [self postMinimumViewWidthChangedNotification]; + } +} + +- (void)updateFontAndLineHeight { + HFLineCountingView *view = [self view]; + HFController *controller = [self controller]; + NSFont *font = controller ? [controller font] : [NSFont fontWithName:HFDEFAULT_FONT size:HFDEFAULT_FONTSIZE]; + [view setFont:font]; + [view setLineHeight: controller ? [controller lineHeight] : HFDEFAULT_FONTSIZE]; + [self updateDigitAdvanceWithFont:font]; +} + +- (void)updateLineNumberFormat { + [[self view] setLineNumberFormat:lineNumberFormat]; +} + +- (void)updateBytesPerLine { + [[self view] setBytesPerLine:[[self controller] bytesPerLine]]; +} + +- (void)updateLineRangeToDraw { + HFFPRange lineRange = {0, 0}; + HFController *controller = [self controller]; + if (controller) { + lineRange = [controller displayedLineRange]; + } + [[self view] setLineRangeToDraw:lineRange]; +} + +- (CGFloat)preferredWidth { + if (digitAdvance == 0) { + /* This may happen if we were loaded from a nib. We are lazy about fetching the controller's font to avoid ordering issues with nib unarchival. */ + [self updateFontAndLineHeight]; + } + return (CGFloat)10. + digitsToRepresentContentsLength * digitAdvance; +} + +- (void)updateMinimumViewWidth { + HFController *controller = [self controller]; + if (controller) { + unsigned long long contentsLength = [controller contentsLength]; + NSUInteger bytesPerLine = [controller bytesPerLine]; + /* We want to know how many lines are displayed. That's equal to the contentsLength divided by bytesPerLine rounded down, except in the case that we're at the end of a line, in which case we need to show one more. Hence adding 1 and dividing gets us the right result. */ + unsigned long long lineCount = contentsLength / bytesPerLine; + unsigned long long contentsLengthRoundedToLine = HFProductULL(lineCount, bytesPerLine) - 1; + NSUInteger digitCount = [HFLineCountingView digitsRequiredToDisplayLineNumber:contentsLengthRoundedToLine inFormat:lineNumberFormat]; + NSUInteger digitWidth = MAX(minimumDigitCount, digitCount); + if (digitWidth != digitsToRepresentContentsLength) { + digitsToRepresentContentsLength = digitWidth; + [self postMinimumViewWidthChangedNotification]; + } + } +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + USE(bytesPerLine); + return [self preferredWidth]; +} + +- (HFLineNumberFormat)lineNumberFormat { + return lineNumberFormat; +} + +- (void)setLineNumberFormat:(HFLineNumberFormat)format { + HFASSERT(format < HFLineNumberFormatMAXIMUM); + lineNumberFormat = format; + [self updateLineNumberFormat]; + [self updateMinimumViewWidth]; +} + + +- (void)cycleLineNumberFormat { + lineNumberFormat = (lineNumberFormat + 1) % HFLineNumberFormatMAXIMUM; + [self updateLineNumberFormat]; + [self updateMinimumViewWidth]; +} + +- (void)initializeView { + [self updateFontAndLineHeight]; + [self updateLineNumberFormat]; + [self updateBytesPerLine]; + [self updateLineRangeToDraw]; + [self updateMinimumViewWidth]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & HFControllerDisplayedLineRange) [self updateLineRangeToDraw]; + if (bits & HFControllerBytesPerLine) [self updateBytesPerLine]; + if (bits & (HFControllerFont | HFControllerLineHeight)) [self updateFontAndLineHeight]; + if (bits & (HFControllerContentLength)) [self updateMinimumViewWidth]; +} + +- (void)setMinimumDigitCount:(NSUInteger)width { + minimumDigitCount = width; + [self updateMinimumViewWidth]; +} + +- (NSUInteger)minimumDigitCount { + return minimumDigitCount; +} + +- (NSUInteger)digitCount { + return digitsToRepresentContentsLength; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(-1, 0); +} + +- (void)setInteriorShadowEdge:(NSInteger)edge { + self->interiorShadowEdge = edge; + if ([self isViewLoaded]) { + [[self view] setNeedsDisplay:YES]; + } +} + +- (NSInteger)interiorShadowEdge { + return interiorShadowEdge; +} + + +- (void)setBorderColor:(NSColor *)color { + [_borderColor autorelease]; + _borderColor = [color copy]; + if ([self isViewLoaded]) { + [[self view] setNeedsDisplay:YES]; + } +} + +- (void)setBackgroundColor:(NSColor *)color { + [_backgroundColor autorelease]; + _backgroundColor = [color copy]; + if ([self isViewLoaded]) { + [[self view] setNeedsDisplay:YES]; + } +} + +@end diff --git a/bsnes/gb/HexFiend/HFLineCountingView.h b/bsnes/gb/HexFiend/HFLineCountingView.h new file mode 100644 index 00000000..2eb90af9 --- /dev/null +++ b/bsnes/gb/HexFiend/HFLineCountingView.h @@ -0,0 +1,32 @@ +// +// HFLineCountingView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +@interface HFLineCountingView : NSView { + NSLayoutManager *layoutManager; + NSTextStorage *textStorage; + NSTextContainer *textContainer; + NSDictionary *textAttributes; + + unsigned long long storedLineIndex; + NSUInteger storedLineCount; + BOOL useStringDrawingPath; + BOOL registeredForAppNotifications; +} + +@property (nonatomic, copy) NSFont *font; +@property (nonatomic) CGFloat lineHeight; +@property (nonatomic) HFFPRange lineRangeToDraw; +@property (nonatomic) NSUInteger bytesPerLine; +@property (nonatomic) HFLineNumberFormat lineNumberFormat; +@property (nonatomic, assign) HFLineCountingRepresenter *representer; + ++ (NSUInteger)digitsRequiredToDisplayLineNumber:(unsigned long long)lineNumber inFormat:(HFLineNumberFormat)format; + +@end diff --git a/bsnes/gb/HexFiend/HFLineCountingView.m b/bsnes/gb/HexFiend/HFLineCountingView.m new file mode 100644 index 00000000..080599b0 --- /dev/null +++ b/bsnes/gb/HexFiend/HFLineCountingView.m @@ -0,0 +1,675 @@ +// +// HFLineCountingView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +#define TIME_LINE_NUMBERS 0 + +#define HEX_LINE_NUMBERS_HAVE_0X_PREFIX 0 + +#define INVALID_LINE_COUNT NSUIntegerMax + +#if TIME_LINE_NUMBERS +@interface HFTimingTextView : NSTextView +@end +@implementation HFTimingTextView +- (void)drawRect:(NSRect)rect { + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + [super drawRect:rect]; + CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); + NSLog(@"TextView line number time: %f", endTime - startTime); +} +@end +#endif + +@implementation HFLineCountingView + +- (void)_sharedInitLineCountingView { + layoutManager = [[NSLayoutManager alloc] init]; + textStorage = [[NSTextStorage alloc] init]; + [textStorage addLayoutManager:layoutManager]; + textContainer = [[NSTextContainer alloc] init]; + [textContainer setLineFragmentPadding:(CGFloat)5]; + [textContainer setContainerSize:NSMakeSize(self.bounds.size.width, [textContainer containerSize].height)]; + [layoutManager addTextContainer:textContainer]; +} + +- (instancetype)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self _sharedInitLineCountingView]; + } + return self; +} + +- (void)dealloc { + HFUnregisterViewForWindowAppearanceChanges(self, registeredForAppNotifications); + [_font release]; + [layoutManager release]; + [textContainer release]; + [textStorage release]; + [textAttributes release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeObject:_font forKey:@"HFFont"]; + [coder encodeDouble:_lineHeight forKey:@"HFLineHeight"]; + [coder encodeObject:_representer forKey:@"HFRepresenter"]; + [coder encodeInt64:_bytesPerLine forKey:@"HFBytesPerLine"]; + [coder encodeInt64:_lineNumberFormat forKey:@"HFLineNumberFormat"]; + [coder encodeBool:useStringDrawingPath forKey:@"HFUseStringDrawingPath"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + [self _sharedInitLineCountingView]; + _font = [[coder decodeObjectForKey:@"HFFont"] retain]; + _lineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFLineHeight"]; + _representer = [coder decodeObjectForKey:@"HFRepresenter"]; + _bytesPerLine = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesPerLine"]; + _lineNumberFormat = (NSUInteger)[coder decodeInt64ForKey:@"HFLineNumberFormat"]; + useStringDrawingPath = [coder decodeBoolForKey:@"HFUseStringDrawingPath"]; + return self; +} + +- (BOOL)isFlipped { return YES; } + +- (void)getLineNumberFormatString:(char *)outString length:(NSUInteger)length { + HFLineNumberFormat format = self.lineNumberFormat; + if (format == HFLineNumberFormatDecimal) { + strlcpy(outString, "%llu", length); + } + else if (format == HFLineNumberFormatHexadecimal) { +#if HEX_LINE_NUMBERS_HAVE_0X_PREFIX + // we want a format string like 0x%08llX + snprintf(outString, length, "0x%%0%lullX", (unsigned long)self.representer.digitCount - 2); +#else + // we want a format string like %08llX + snprintf(outString, length, "%%0%lullX", (unsigned long)self.representer.digitCount); +#endif + } + else { + strlcpy(outString, "", length); + } +} + +- (void)windowDidChangeKeyStatus:(NSNotification *)note { + USE(note); + [self setNeedsDisplay:YES]; +} + +- (void)viewDidMoveToWindow { + HFRegisterViewForWindowAppearanceChanges(self, @selector(windowDidChangeKeyStatus:), !registeredForAppNotifications); + registeredForAppNotifications = YES; + [super viewDidMoveToWindow]; +} + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow { + HFUnregisterViewForWindowAppearanceChanges(self, NO); + [super viewWillMoveToWindow:newWindow]; +} + +- (void)drawDividerWithClip:(NSRect)clipRect { + USE(clipRect); + + +#if 1 + NSInteger edges = _representer.borderedEdges; + NSRect bounds = self.bounds; + + + // -1 means to draw no edges + if (edges == -1) { + edges = 0; + } + + [_representer.borderColor set]; + + if ((edges & (1 << NSMinXEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.width = 1; + lineRect.origin.x = 0; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + if ((edges & (1 << NSMaxXEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.width = 1; + lineRect.origin.x = NSMaxX(bounds) - lineRect.size.width; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + if ((edges & (1 << NSMinYEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.height = 1; + lineRect.origin.y = 0; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + if ((edges & (1 << NSMaxYEdge)) > 0) { + NSRect lineRect = bounds; + lineRect.size.height = 1; + lineRect.origin.y = NSMaxY(bounds) - lineRect.size.height; + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + } + + + // Backwards compatibility to always draw a border on the edge with the interior shadow + + NSRect lineRect = bounds; + lineRect.size.width = 1; + NSInteger shadowEdge = _representer.interiorShadowEdge; + if (shadowEdge == NSMaxXEdge) { + lineRect.origin.x = NSMaxX(bounds) - lineRect.size.width; + } else if (shadowEdge == NSMinXEdge) { + lineRect.origin.x = NSMinX(bounds); + } else { + lineRect = NSZeroRect; + } + + if (NSIntersectsRect(lineRect, clipRect)) { + NSRectFill(lineRect); + } + +#else + + + if (NSIntersectsRect(lineRect, clipRect)) { + // this looks better when we have no shadow + [[NSColor lightGrayColor] set]; + NSRect bounds = self.bounds; + NSRect lineRect = bounds; + lineRect.origin.x += lineRect.size.width - 2; + lineRect.size.width = 1; + NSRectFill(NSIntersectionRect(lineRect, clipRect)); + [[NSColor whiteColor] set]; + lineRect.origin.x += 1; + NSRectFill(NSIntersectionRect(lineRect, clipRect)); + } +#endif +} + +static inline int common_prefix_length(const char *a, const char *b) { + int i; + for (i=0; ; i++) { + char ac = a[i]; + char bc = b[i]; + if (ac != bc || ac == 0 || bc == 0) break; + } + return i; +} + +/* Drawing with NSLayoutManager is necessary because the 10_2 typesetting behavior used by the old string drawing does the wrong thing for fonts like Bitstream Vera Sans Mono. Also it's an optimization for drawing the shadow. */ +- (void)drawLineNumbersWithClipLayoutManagerPerLine:(NSRect)clipRect { +#if TIME_LINE_NUMBERS + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); +#endif + NSUInteger previousTextStorageCharacterCount = [textStorage length]; + + CGFloat verticalOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + NSRect textRect = self.bounds; + textRect.size.height = _lineHeight; + textRect.origin.y -= verticalOffset * _lineHeight; + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + unsigned long long lineValue = lineIndex * _bytesPerLine; + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + char previousBuff[256]; + int previousStringLength = (int)previousTextStorageCharacterCount; + BOOL conversionResult = [[textStorage string] getCString:previousBuff maxLength:sizeof previousBuff encoding:NSASCIIStringEncoding]; + HFASSERT(conversionResult); + while (linesRemaining--) { + char formatString[64]; + [self getLineNumberFormatString:formatString length:sizeof formatString]; + + if (NSIntersectsRect(textRect, clipRect)) { + NSString *replacementCharacters = nil; + NSRange replacementRange; + char buff[256]; + int newStringLength = snprintf(buff, sizeof buff, formatString, lineValue); + HFASSERT(newStringLength > 0); + int prefixLength = common_prefix_length(previousBuff, buff); + HFASSERT(prefixLength <= newStringLength); + HFASSERT(prefixLength <= previousStringLength); + replacementRange = NSMakeRange(prefixLength, previousStringLength - prefixLength); + replacementCharacters = [[NSString alloc] initWithBytesNoCopy:buff + prefixLength length:newStringLength - prefixLength encoding:NSASCIIStringEncoding freeWhenDone:NO]; + NSUInteger glyphCount; + [textStorage replaceCharactersInRange:replacementRange withString:replacementCharacters]; + if (previousTextStorageCharacterCount == 0) { + NSDictionary *atts = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, nil]; + [textStorage setAttributes:atts range:NSMakeRange(0, newStringLength)]; + [atts release]; + } + glyphCount = [layoutManager numberOfGlyphs]; + if (glyphCount > 0) { + CGFloat maxX = NSMaxX([layoutManager lineFragmentUsedRectForGlyphAtIndex:glyphCount - 1 effectiveRange:NULL]); + [layoutManager drawGlyphsForGlyphRange:NSMakeRange(0, glyphCount) atPoint:NSMakePoint(textRect.origin.x + textRect.size.width - maxX, textRect.origin.y)]; + } + previousTextStorageCharacterCount = newStringLength; + [replacementCharacters release]; + memcpy(previousBuff, buff, newStringLength + 1); + previousStringLength = newStringLength; + } + textRect.origin.y += _lineHeight; + lineIndex++; + lineValue = HFSum(lineValue, _bytesPerLine); + } +#if TIME_LINE_NUMBERS + CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); + NSLog(@"Line number time: %f", endTime - startTime); +#endif +} + +- (void)drawLineNumbersWithClipStringDrawing:(NSRect)clipRect { + CGFloat verticalOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + NSRect textRect = self.bounds; + textRect.size.height = _lineHeight; + textRect.size.width -= 5; + textRect.origin.y -= verticalOffset * _lineHeight + 1; + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + unsigned long long lineValue = lineIndex * _bytesPerLine; + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + if (! textAttributes) { + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + } + + char formatString[64]; + [self getLineNumberFormatString:formatString length:sizeof formatString]; + + while (linesRemaining--) { + if (NSIntersectsRect(textRect, clipRect)) { + char buff[256]; + int newStringLength = snprintf(buff, sizeof buff, formatString, lineValue); + HFASSERT(newStringLength > 0); + NSString *string = [[NSString alloc] initWithBytesNoCopy:buff length:newStringLength encoding:NSASCIIStringEncoding freeWhenDone:NO]; + [string drawInRect:textRect withAttributes:textAttributes]; + [string release]; + } + textRect.origin.y += _lineHeight; + lineIndex++; + if (linesRemaining > 0) lineValue = HFSum(lineValue, _bytesPerLine); //we could do this unconditionally, but then we risk overflow + } +} + +- (NSUInteger)characterCountForLineRange:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); + NSUInteger characterCount; + + NSUInteger lineCount = ll2l(range.length); + const NSUInteger stride = _bytesPerLine; + HFLineCountingRepresenter *rep = self.representer; + HFLineNumberFormat format = self.lineNumberFormat; + if (format == HFLineNumberFormatDecimal) { + unsigned long long lineValue = HFProductULL(range.location, _bytesPerLine); + characterCount = lineCount /* newlines */; + while (lineCount--) { + characterCount += HFCountDigitsBase10(lineValue); + lineValue += stride; + } + } + else if (format == HFLineNumberFormatHexadecimal) { + characterCount = ([rep digitCount] + 1) * lineCount; // +1 for newlines + } + else { + characterCount = -1; + } + return characterCount; +} + +- (NSString *)newLineStringForRange:(HFRange)range { + HFASSERT(range.length <= NSUIntegerMax); + if(range.length == 0) + return [[NSString alloc] init]; // Placate the analyzer. + + NSUInteger lineCount = ll2l(range.length); + const NSUInteger stride = _bytesPerLine; + unsigned long long lineValue = HFProductULL(range.location, _bytesPerLine); + NSUInteger characterCount = [self characterCountForLineRange:range]; + char *buffer = check_malloc(characterCount); + NSUInteger bufferIndex = 0; + + char formatString[64]; + [self getLineNumberFormatString:formatString length:sizeof formatString]; + + while (lineCount--) { + int charCount = sprintf(buffer + bufferIndex, formatString, lineValue + self.representer.valueOffset); + HFASSERT(charCount > 0); + bufferIndex += charCount; + buffer[bufferIndex++] = '\n'; + lineValue += stride; + } + HFASSERT(bufferIndex == characterCount); + + NSString *string = [[NSString alloc] initWithBytesNoCopy:(void *)buffer length:bufferIndex encoding:NSASCIIStringEncoding freeWhenDone:YES]; + return string; +} + +- (void)updateLayoutManagerWithLineIndex:(unsigned long long)startingLineIndex lineCount:(NSUInteger)linesRemaining { + const BOOL debug = NO; + [textStorage beginEditing]; + + if (storedLineCount == INVALID_LINE_COUNT) { + /* This usually indicates that our bytes per line or line number format changed, and we need to just recalculate everything */ + NSString *string = [self newLineStringForRange:HFRangeMake(startingLineIndex, linesRemaining)]; + [textStorage replaceCharactersInRange:NSMakeRange(0, [textStorage length]) withString:string]; + [string release]; + + } + else { + HFRange leftRangeToReplace, rightRangeToReplace; + HFRange leftRangeToStore, rightRangeToStore; + + HFRange oldRange = HFRangeMake(storedLineIndex, storedLineCount); + HFRange newRange = HFRangeMake(startingLineIndex, linesRemaining); + HFRange rangeToPreserve = HFIntersectionRange(oldRange, newRange); + + if (rangeToPreserve.length == 0) { + leftRangeToReplace = oldRange; + leftRangeToStore = newRange; + rightRangeToReplace = HFZeroRange; + rightRangeToStore = HFZeroRange; + } + else { + if (debug) NSLog(@"Preserving %llu", rangeToPreserve.length); + HFASSERT(HFRangeIsSubrangeOfRange(rangeToPreserve, newRange)); + HFASSERT(HFRangeIsSubrangeOfRange(rangeToPreserve, oldRange)); + const unsigned long long maxPreserve = HFMaxRange(rangeToPreserve); + leftRangeToReplace = HFRangeMake(oldRange.location, rangeToPreserve.location - oldRange.location); + leftRangeToStore = HFRangeMake(newRange.location, rangeToPreserve.location - newRange.location); + rightRangeToReplace = HFRangeMake(maxPreserve, HFMaxRange(oldRange) - maxPreserve); + rightRangeToStore = HFRangeMake(maxPreserve, HFMaxRange(newRange) - maxPreserve); + } + + if (debug) NSLog(@"Changing %@ -> %@", HFRangeToString(oldRange), HFRangeToString(newRange)); + if (debug) NSLog(@"LEFT: %@ -> %@", HFRangeToString(leftRangeToReplace), HFRangeToString(leftRangeToStore)); + if (debug) NSLog(@"RIGHT: %@ -> %@", HFRangeToString(rightRangeToReplace), HFRangeToString(rightRangeToStore)); + + HFASSERT(leftRangeToReplace.length == 0 || HFRangeIsSubrangeOfRange(leftRangeToReplace, oldRange)); + HFASSERT(rightRangeToReplace.length == 0 || HFRangeIsSubrangeOfRange(rightRangeToReplace, oldRange)); + + if (leftRangeToReplace.length > 0 || leftRangeToStore.length > 0) { + NSUInteger charactersToDelete = [self characterCountForLineRange:leftRangeToReplace]; + NSRange rangeToDelete = NSMakeRange(0, charactersToDelete); + if (leftRangeToStore.length == 0) { + [textStorage deleteCharactersInRange:rangeToDelete]; + if (debug) NSLog(@"Left deleting text range %@", NSStringFromRange(rangeToDelete)); + } + else { + NSString *leftRangeString = [self newLineStringForRange:leftRangeToStore]; + [textStorage replaceCharactersInRange:rangeToDelete withString:leftRangeString]; + if (debug) NSLog(@"Replacing text range %@ with %@", NSStringFromRange(rangeToDelete), leftRangeString); + [leftRangeString release]; + } + } + + if (rightRangeToReplace.length > 0 || rightRangeToStore.length > 0) { + NSUInteger charactersToDelete = [self characterCountForLineRange:rightRangeToReplace]; + NSUInteger stringLength = [textStorage length]; + HFASSERT(charactersToDelete <= stringLength); + NSRange rangeToDelete = NSMakeRange(stringLength - charactersToDelete, charactersToDelete); + if (rightRangeToStore.length == 0) { + [textStorage deleteCharactersInRange:rangeToDelete]; + if (debug) NSLog(@"Right deleting text range %@", NSStringFromRange(rangeToDelete)); + } + else { + NSString *rightRangeString = [self newLineStringForRange:rightRangeToStore]; + [textStorage replaceCharactersInRange:rangeToDelete withString:rightRangeString]; + if (debug) NSLog(@"Replacing text range %@ with %@ (for range %@)", NSStringFromRange(rangeToDelete), rightRangeString, HFRangeToString(rightRangeToStore)); + [rightRangeString release]; + } + } + } + + if (!textAttributes) { + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + [textStorage setAttributes:textAttributes range:NSMakeRange(0, [textStorage length])]; + } + + [textStorage endEditing]; + +#if ! NDEBUG + NSString *comparisonString = [self newLineStringForRange:HFRangeMake(startingLineIndex, linesRemaining)]; + if (! [comparisonString isEqualToString:[textStorage string]]) { + NSLog(@"Not equal!"); + NSLog(@"Expected:\n%@", comparisonString); + NSLog(@"Actual:\n%@", [textStorage string]); + } + HFASSERT([comparisonString isEqualToString:[textStorage string]]); + [comparisonString release]; +#endif + + storedLineIndex = startingLineIndex; + storedLineCount = linesRemaining; +} + +- (void)drawLineNumbersWithClipSingleStringDrawing:(NSRect)clipRect { + USE(clipRect); + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + + CGFloat linesToVerticallyOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + CGFloat verticalOffset = linesToVerticallyOffset * _lineHeight + 1.5; + NSRect textRect = self.bounds; + textRect.size.width -= 5; + textRect.origin.y -= verticalOffset; + textRect.size.height += verticalOffset + _lineHeight; + + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + [mutableStyle setMinimumLineHeight:_lineHeight]; + [mutableStyle setMaximumLineHeight:_lineHeight]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + NSColor *color = [[NSColor textColor] colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + color = [NSColor colorWithRed:color.redComponent green:color.greenComponent blue:color.blueComponent alpha:0.75]; + NSDictionary *_textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSFont fontWithName:_font.fontName size:9], NSFontAttributeName, color, NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + + + NSString *string = [self newLineStringForRange:HFRangeMake(lineIndex, linesRemaining)]; + [string drawInRect:textRect withAttributes:_textAttributes]; + [string release]; + [_textAttributes release]; +} + +- (void)drawLineNumbersWithClipSingleStringCellDrawing:(NSRect)clipRect { + USE(clipRect); + const CGFloat cellTextContainerPadding = 2.f; + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + + CGFloat linesToVerticallyOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + CGFloat verticalOffset = linesToVerticallyOffset * _lineHeight + 1; + NSRect textRect = self.bounds; + textRect.size.width -= 5; + textRect.origin.y -= verticalOffset; + textRect.origin.x += cellTextContainerPadding; + textRect.size.height += verticalOffset; + + if (! textAttributes) { + NSMutableParagraphStyle *mutableStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [mutableStyle setAlignment:NSRightTextAlignment]; + [mutableStyle setMinimumLineHeight:_lineHeight]; + [mutableStyle setMaximumLineHeight:_lineHeight]; + NSParagraphStyle *paragraphStyle = [mutableStyle copy]; + [mutableStyle release]; + textAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:_font, NSFontAttributeName, [NSColor controlTextColor], NSForegroundColorAttributeName, paragraphStyle, NSParagraphStyleAttributeName, nil]; + [paragraphStyle release]; + } + + NSString *string = [self newLineStringForRange:HFRangeMake(lineIndex, linesRemaining)]; + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:textAttributes]; + [string release]; + NSCell *cell = [[NSCell alloc] initTextCell:@""]; + [cell setAttributedStringValue:attributedString]; + [cell drawWithFrame:textRect inView:self]; + [[NSColor purpleColor] set]; + NSFrameRect(textRect); + [cell release]; + [attributedString release]; +} + +- (void)drawLineNumbersWithClipFullLayoutManager:(NSRect)clipRect { + USE(clipRect); + unsigned long long lineIndex = HFFPToUL(floorl(_lineRangeToDraw.location)); + NSUInteger linesRemaining = ll2l(HFFPToUL(ceill(_lineRangeToDraw.length + _lineRangeToDraw.location) - floorl(_lineRangeToDraw.location))); + if (lineIndex != storedLineIndex || linesRemaining != storedLineCount) { + [self updateLayoutManagerWithLineIndex:lineIndex lineCount:linesRemaining]; + } + + CGFloat verticalOffset = ld2f(_lineRangeToDraw.location - floorl(_lineRangeToDraw.location)); + + NSPoint textPoint = self.bounds.origin; + textPoint.y -= verticalOffset * _lineHeight; + [layoutManager drawGlyphsForGlyphRange:NSMakeRange(0, [layoutManager numberOfGlyphs]) atPoint:textPoint]; +} + +- (void)drawLineNumbersWithClip:(NSRect)clipRect { +#if TIME_LINE_NUMBERS + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); +#endif + NSInteger drawingMode = 3; // (useStringDrawingPath ? 1 : 3); + switch (drawingMode) { + // Drawing can't be done right if fonts are wider than expected, but all + // of these have rather nasty behavior in that case. I've commented what + // that behavior is; the comment is hypothetical 'could' if it shouldn't + // actually be a problem in practice. + // TODO: Make a drawing mode that is "Fonts could get clipped if too wide" + // because that seems like better behavior than any of these. + case 0: + // Most fonts are too wide and every character gets piled on right (unreadable). + [self drawLineNumbersWithClipLayoutManagerPerLine:clipRect]; + break; + case 1: + // Last characters could get omitted (*not* clipped) if too wide. + // Also, most fonts have bottoms clipped (very unsigntly). + [self drawLineNumbersWithClipStringDrawing:clipRect]; + break; + case 2: + // Most fonts are too wide and wrap (breaks numbering). + [self drawLineNumbersWithClipFullLayoutManager:clipRect]; + break; + case 3: + // Fonts could wrap if too wide (breaks numbering). + // *Note that that this is the only mode that generally works.* + [self drawLineNumbersWithClipSingleStringDrawing:clipRect]; + break; + case 4: + // Most fonts are too wide and wrap (breaks numbering). + [self drawLineNumbersWithClipSingleStringCellDrawing:clipRect]; + break; + } +#if TIME_LINE_NUMBERS + CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); + NSLog(@"Line number time: %f", endTime - startTime); +#endif +} + +- (void)drawRect:(NSRect)clipRect { + [self drawDividerWithClip:clipRect]; + [self drawLineNumbersWithClip:clipRect]; +} + +- (void)setLineRangeToDraw:(HFFPRange)range { + if (! HFFPRangeEqualsRange(range, _lineRangeToDraw)) { + _lineRangeToDraw = range; + [self setNeedsDisplay:YES]; + } +} + +- (void)setBytesPerLine:(NSUInteger)val { + if (_bytesPerLine != val) { + _bytesPerLine = val; + storedLineCount = INVALID_LINE_COUNT; + [self setNeedsDisplay:YES]; + } +} + +- (void)setLineNumberFormat:(HFLineNumberFormat)format { + if (format != _lineNumberFormat) { + _lineNumberFormat = format; + storedLineCount = INVALID_LINE_COUNT; + [self setNeedsDisplay:YES]; + } +} + +- (BOOL)canUseStringDrawingPathForFont:(NSFont *)testFont { + NSString *name = [testFont fontName]; + // No, Menlo does not work here. + return [name isEqualToString:@"Monaco"] || [name isEqualToString:@"Courier"] || [name isEqualToString:@"Consolas"]; +} + +- (void)setFont:(NSFont *)val { + if (val != _font) { + [_font release]; + _font = [val copy]; + [textStorage deleteCharactersInRange:NSMakeRange(0, [textStorage length])]; //delete the characters so we know to set the font next time we render + [textAttributes release]; + textAttributes = nil; + storedLineCount = INVALID_LINE_COUNT; + useStringDrawingPath = [self canUseStringDrawingPathForFont:_font]; + [self setNeedsDisplay:YES]; + } +} + +- (void)setLineHeight:(CGFloat)height { + if (_lineHeight != height) { + _lineHeight = height; + [self setNeedsDisplay:YES]; + } +} + +- (void)setFrameSize:(NSSize)size { + [super setFrameSize:size]; + [textContainer setContainerSize:NSMakeSize(self.bounds.size.width, [textContainer containerSize].height)]; +} + +- (void)mouseDown:(NSEvent *)event { + USE(event); + // [_representer cycleLineNumberFormat]; +} + +- (void)scrollWheel:(NSEvent *)scrollEvent { + [_representer.controller scrollWithScrollEvent:scrollEvent]; +} + ++ (NSUInteger)digitsRequiredToDisplayLineNumber:(unsigned long long)lineNumber inFormat:(HFLineNumberFormat)format { + switch (format) { + case HFLineNumberFormatDecimal: return HFCountDigitsBase10(lineNumber); +#if HEX_LINE_NUMBERS_HAVE_0X_PREFIX + case HFLineNumberFormatHexadecimal: return 2 + HFCountDigitsBase16(lineNumber); +#else + case HFLineNumberFormatHexadecimal: return HFCountDigitsBase16(lineNumber); +#endif + default: return 0; + } +} + +@end diff --git a/bsnes/gb/HexFiend/HFPasteboardOwner.h b/bsnes/gb/HexFiend/HFPasteboardOwner.h new file mode 100644 index 00000000..d09c5794 --- /dev/null +++ b/bsnes/gb/HexFiend/HFPasteboardOwner.h @@ -0,0 +1,51 @@ +// +// HFPasteboardOwner.h +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import + +@class HFByteArray; + +extern NSString *const HFPrivateByteArrayPboardType; + +@interface HFPasteboardOwner : NSObject { + @private + HFByteArray *byteArray; + NSPasteboard *pasteboard; //not retained + unsigned long long dataAmountToCopy; + NSUInteger bytesPerLine; + BOOL retainedSelfOnBehalfOfPboard; + BOOL backgroundCopyOperationFinished; + BOOL didStartModalSessionForBackgroundCopyOperation; +} + +/* Creates an HFPasteboardOwner to own the given pasteboard with the given types. Note that the NSPasteboard retains its owner. */ ++ (id)ownPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types; +- (HFByteArray *)byteArray; + +/* Performs a copy to pasteboard with progress reporting. This must be overridden if you support types other than the private pboard type. */ +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker; + +/* NSPasteboard delegate methods, declared here to indicate that subclasses should call super */ +- (void)pasteboard:(NSPasteboard *)sender provideDataForType:(NSString *)type; +- (void)pasteboardChangedOwner:(NSPasteboard *)pboard; + +/* Useful property that several pasteboard types want to know */ +@property (nonatomic) NSUInteger bytesPerLine; + +/* For efficiency, Hex Fiend writes pointers to HFByteArrays into pasteboards. In the case that the user quits and relaunches Hex Fiend, we don't want to read a pointer from the old process, so each process we generate a UUID. This is constant for the lifetime of the process. */ ++ (NSString *)uuid; + +/* Unpacks a byte array from a pasteboard, preferring HFPrivateByteArrayPboardType */ ++ (HFByteArray *)unpackByteArrayFromPasteboard:(NSPasteboard *)pasteboard; + +/* Used to handle the case where copying data will require a lot of memory and give the user a chance to confirm. */ +- (unsigned long long)amountToCopyForDataLength:(unsigned long long)numBytes stringLength:(unsigned long long)stringLength; + +/* Must be overridden to return the length of a string containing this number of bytes. */ +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength; + +@end diff --git a/bsnes/gb/HexFiend/HFPasteboardOwner.m b/bsnes/gb/HexFiend/HFPasteboardOwner.m new file mode 100755 index 00000000..0ca341d6 --- /dev/null +++ b/bsnes/gb/HexFiend/HFPasteboardOwner.m @@ -0,0 +1,287 @@ +// +// HFPasteboardOwner.m +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import + +NSString *const HFPrivateByteArrayPboardType = @"HFPrivateByteArrayPboardType"; + +@implementation HFPasteboardOwner + ++ (void)initialize { + if (self == [HFPasteboardOwner class]) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareCommonPasteboardsForChangeInFileNotification:) name:HFPrepareForChangeInFileNotification object:nil]; + } +} + +- (instancetype)initWithPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types { + REQUIRE_NOT_NULL(pboard); + REQUIRE_NOT_NULL(array); + REQUIRE_NOT_NULL(types); + self = [super init]; + byteArray = [array retain]; + pasteboard = pboard; + [pasteboard declareTypes:types owner:self]; + + // get notified when we're about to write a file, so that if they're overwriting a file backing part of our byte array, we can properly clear or preserve our pasteboard + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeInFileNotification:) name:HFPrepareForChangeInFileNotification object:nil]; + + return self; +} ++ (id)ownPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types { + return [[[self alloc] initWithPasteboard:pboard forByteArray:array withTypes:types] autorelease]; +} + +- (void)tearDownPasteboardReferenceIfExists { + if (pasteboard) { + pasteboard = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self name:HFPrepareForChangeInFileNotification object:nil]; + } + if (retainedSelfOnBehalfOfPboard) { + CFRelease(self); + retainedSelfOnBehalfOfPboard = NO; + } +} + + ++ (HFByteArray *)_unpackByteArrayFromDictionary:(NSDictionary *)byteArrayDictionary { + HFByteArray *result = nil; + if (byteArrayDictionary) { + NSString *uuid = byteArrayDictionary[@"HFUUID"]; + if ([uuid isEqual:[self uuid]]) { + result = (HFByteArray *)[byteArrayDictionary[@"HFByteArray"] unsignedLongValue]; + } + } + return result; +} + ++ (HFByteArray *)unpackByteArrayFromPasteboard:(NSPasteboard *)pasteboard { + REQUIRE_NOT_NULL(pasteboard); + HFByteArray *result = [self _unpackByteArrayFromDictionary:[pasteboard propertyListForType:HFPrivateByteArrayPboardType]]; + return result; +} + +/* Try to fix up commonly named pasteboards when a file is about to be saved */ ++ (void)prepareCommonPasteboardsForChangeInFileNotification:(NSNotification *)notification { + const BOOL *cancellationPointer = [[notification userInfo][HFChangeInFileShouldCancelKey] pointerValue]; + if (*cancellationPointer) return; //don't do anything if someone requested cancellation + + NSDictionary *userInfo = [notification userInfo]; + NSArray *changedRanges = userInfo[HFChangeInFileModifiedRangesKey]; + HFFileReference *fileReference = [notification object]; + NSMutableDictionary *hint = userInfo[HFChangeInFileHintKey]; + + NSString * const names[] = {NSGeneralPboard, NSFindPboard, NSDragPboard}; + NSUInteger i; + for (i=0; i < sizeof names / sizeof *names; i++) { + NSPasteboard *pboard = [NSPasteboard pasteboardWithName:names[i]]; + HFByteArray *byteArray = [self unpackByteArrayFromPasteboard:pboard]; + if (byteArray && ! [byteArray clearDependenciesOnRanges:changedRanges inFile:fileReference hint:hint]) { + /* This pasteboard no longer works */ + [pboard declareTypes:@[] owner:nil]; + } + } +} + +- (void)changeInFileNotification:(NSNotification *)notification { + HFASSERT(pasteboard != nil); + HFASSERT(byteArray != nil); + NSDictionary *userInfo = [notification userInfo]; + const BOOL *cancellationPointer = [userInfo[HFChangeInFileShouldCancelKey] pointerValue]; + if (*cancellationPointer) return; //don't do anything if someone requested cancellation + NSMutableDictionary *hint = userInfo[HFChangeInFileHintKey]; + + NSArray *changedRanges = [notification userInfo][HFChangeInFileModifiedRangesKey]; + HFFileReference *fileReference = [notification object]; + if (! [byteArray clearDependenciesOnRanges:changedRanges inFile:fileReference hint:hint]) { + /* We can't do it */ + [self tearDownPasteboardReferenceIfExists]; + } +} + +- (void)dealloc { + [self tearDownPasteboardReferenceIfExists]; + [byteArray release]; + [super dealloc]; +} + +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker { + USE(length); + USE(pboard); + USE(type); + USE(tracker); + UNIMPLEMENTED_VOID(); +} + +- (void)backgroundMoveDataToPasteboard:(NSString *)type { + @autoreleasepool { + [self writeDataInBackgroundToPasteboard:pasteboard ofLength:dataAmountToCopy forType:type trackingProgress:nil]; + [self performSelectorOnMainThread:@selector(backgroundMoveDataFinished:) withObject:nil waitUntilDone:NO]; + } +} + +- (void)backgroundMoveDataFinished:unused { + USE(unused); + HFASSERT(backgroundCopyOperationFinished == NO); + backgroundCopyOperationFinished = YES; + if (! didStartModalSessionForBackgroundCopyOperation) { + /* We haven't started the modal session, so make sure it never happens */ + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(beginModalSessionForBackgroundCopyOperation:) object:nil]; + CFRunLoopWakeUp(CFRunLoopGetCurrent()); + } + else { + /* We have started the modal session, so end it. */ + [NSApp stopModalWithCode:0]; + //stopModal: won't trigger unless we post a do-nothing event + NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:0 data1:0 data2:0]; + [NSApp postEvent:event atStart:NO]; + } +} + +- (void)beginModalSessionForBackgroundCopyOperation:(id)unused { + USE(unused); + HFASSERT(backgroundCopyOperationFinished == NO); + HFASSERT(didStartModalSessionForBackgroundCopyOperation == NO); + didStartModalSessionForBackgroundCopyOperation = YES; +} + +- (BOOL)moveDataWithProgressReportingToPasteboard:(NSPasteboard *)pboard forType:(NSString *)type { + // The -[NSRunLoop runMode:beforeDate:] call in the middle of this function can cause it to be + // called reentrantly, which was previously causing leaks and use-after-free crashes. For + // some reason this happens basically always when copying lots of data into VMware Fusion. + // I'm not even sure what the ideal behavior would be here, but am fairly certain that this + // is the best that can be done without rewriting a portion of the background copying code. + // TODO: Figure out what the ideal behavior should be here. + + HFASSERT(pboard == pasteboard); + [self retain]; //resolving the pasteboard may release us, which deallocates us, which deallocates our tracker...make sure we survive through this function + /* Give the user a chance to request a smaller amount if it's really big */ + unsigned long long availableAmount = [byteArray length]; + unsigned long long amountToCopy = [self amountToCopyForDataLength:availableAmount stringLength:[self stringLengthForDataLength:availableAmount]]; + if (amountToCopy > 0) { + + backgroundCopyOperationFinished = NO; + didStartModalSessionForBackgroundCopyOperation = NO; + dataAmountToCopy = amountToCopy; + [NSThread detachNewThreadSelector:@selector(backgroundMoveDataToPasteboard:) toTarget:self withObject:type]; + [self performSelector:@selector(beginModalSessionForBackgroundCopyOperation:) withObject:nil afterDelay:1.0 inModes:@[NSModalPanelRunLoopMode]]; + while (! backgroundCopyOperationFinished) { + [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate distantFuture]]; + } + } + [self release]; + return YES; +} + +- (void)pasteboardChangedOwner:(NSPasteboard *)pboard { + HFASSERT(pasteboard == pboard); + [self tearDownPasteboardReferenceIfExists]; +} + +- (HFByteArray *)byteArray { + return byteArray; +} + +- (void)pasteboard:(NSPasteboard *)pboard provideDataForType:(NSString *)type { + if (! pasteboard) { + /* Don't do anything, because we've torn down our pasteboard */ + return; + } + if ([type isEqualToString:HFPrivateByteArrayPboardType]) { + if (! retainedSelfOnBehalfOfPboard) { + retainedSelfOnBehalfOfPboard = YES; + CFRetain(self); + } + NSDictionary *dict = @{@"HFByteArray": @((unsigned long)byteArray), + @"HFUUID": [[self class] uuid]}; + [pboard setPropertyList:dict forType:type]; + } + else { + if (! [self moveDataWithProgressReportingToPasteboard:pboard forType:type]) { + [pboard setData:[NSData data] forType:type]; + } + } +} + +- (void)setBytesPerLine:(NSUInteger)val { bytesPerLine = val; } +- (NSUInteger)bytesPerLine { return bytesPerLine; } + ++ (NSString *)uuid { + static NSString *uuid; + if (! uuid) { + CFUUIDRef uuidRef = CFUUIDCreate(NULL); + uuid = (NSString *)CFUUIDCreateString(NULL, uuidRef); + CFRelease(uuidRef); + } + return uuid; +} + +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength { USE(dataLength); UNIMPLEMENTED(); } + +- (unsigned long long)amountToCopyForDataLength:(unsigned long long)numBytes stringLength:(unsigned long long)stringLength { + unsigned long long dataLengthResult, stringLengthResult; + NSInteger alertReturn = NSIntegerMax; + const unsigned long long copyOption1 = MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT; + const unsigned long long copyOption2 = MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT; + NSString *option1String = HFDescribeByteCount(copyOption1); + NSString *option2String = HFDescribeByteCount(copyOption2); + NSString* dataSizeDescription = HFDescribeByteCount(stringLength); + if (stringLength >= MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT) { + NSString *option1 = [@"Copy " stringByAppendingString:option1String]; + NSString *option2 = [@"Copy " stringByAppendingString:option2String]; + alertReturn = NSRunAlertPanel(@"Large Clipboard", @"The copied data would occupy %@ if written to the clipboard. This is larger than the system clipboard supports. Do you want to copy only part of the data?", @"Cancel", option1, option2, dataSizeDescription); + switch (alertReturn) { + case NSAlertDefaultReturn: + default: + stringLengthResult = 0; + break; + case NSAlertAlternateReturn: + stringLengthResult = copyOption1; + break; + case NSAlertOtherReturn: + stringLengthResult = copyOption2; + break; + } + + } + else if (stringLength >= MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT) { + NSString *option1 = [@"Copy " stringByAppendingString:HFDescribeByteCount(stringLength)]; + NSString *option2 = [@"Copy " stringByAppendingString:HFDescribeByteCount(copyOption2)]; + alertReturn = NSRunAlertPanel(@"Large Clipboard", @"The copied data would occupy %@ if written to the clipboard. Performing this copy may take a long time. Do you want to copy only part of the data?", @"Cancel", option1, option2, dataSizeDescription); + switch (alertReturn) { + case NSAlertDefaultReturn: + default: + stringLengthResult = 0; + break; + case NSAlertAlternateReturn: + stringLengthResult = stringLength; + break; + case NSAlertOtherReturn: + stringLengthResult = copyOption2; + break; + } + } + else { + /* Small enough to copy it all */ + stringLengthResult = stringLength; + } + + /* Convert from string length to data length */ + if (stringLengthResult == stringLength) { + dataLengthResult = numBytes; + } + else { + unsigned long long divisor = stringLength / numBytes; + dataLengthResult = stringLengthResult / divisor; + } + + return dataLengthResult; +} + +@end diff --git a/bsnes/gb/HexFiend/HFPrivilegedHelperConnection.h b/bsnes/gb/HexFiend/HFPrivilegedHelperConnection.h new file mode 100644 index 00000000..bbee7a9a --- /dev/null +++ b/bsnes/gb/HexFiend/HFPrivilegedHelperConnection.h @@ -0,0 +1,3 @@ +#ifndef HF_NO_PRIVILEGED_FILE_OPERATIONS +#define HF_NO_PRIVILEGED_FILE_OPERATIONS +#endif \ No newline at end of file diff --git a/bsnes/gb/HexFiend/HFRepresenter.h b/bsnes/gb/HexFiend/HFRepresenter.h new file mode 100644 index 00000000..1a15f3e7 --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenter.h @@ -0,0 +1,121 @@ +// +// HFRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +/*! @class HFRepresenter + @brief The principal view class of Hex Fiend's MVC architecture. + + HFRepresenter is a class that visually represents some property of the HFController, such as the data (in various formats), the scroll position, the line number, etc. An HFRepresenter is added to an HFController and then gets notified of changes to various properties, through the controllerDidChange: methods. + + HFRepresenters also have a view, accessible through the -view method. The HFRepresenter is expected to update its view to reflect the relevant properties of its HFController. If the user can interact with the view, then the HFRepresenter should pass any changes down to the HFController, which will subsequently notify all HFRepresenters of the change. + + HFRepresenter is an abstract class, with a different subclass for each possible view type. Because HFController interacts with HFRepresenters, rather than views directly, an HFRepresenter can use standard Cocoa views and controls. + + To add a new view type: + + -# Create a subclass of HFRepresenter + -# Override \c -createView to return a view (note that this method should transfer ownership) + -# Override \c -controllerDidChange:, checking the bitmask to see what properties have changed and updating your view as appropriate + -# If you plan on using this view together with other views, override \c +defaultLayoutPosition to control how your view gets positioned in an HFLayoutRepresenter + -# If your view's width depends on the properties of the controller, override some of the measurement methods, such as \c +maximumBytesPerLineForViewWidth:, so that your view gets sized correctly + +*/ +@interface HFRepresenter : NSObject { + @private + id view; + HFController *controller; + NSPoint layoutPosition; +} + +/*! @name View management + Methods related to accessing and initializing the representer's view. +*/ +//@{ +/*! Returns the view for the receiver, creating it if necessary. The view for the HFRepresenter is initially nil. When the \c -view method is called, if the view is nil, \c -createView is called and then the result is stored. This method should not be overridden; however you may want to call it to access the view. +*/ +- (id)view; + +/*! Returns YES if the view has been created, NO if it has not. To create the view, call the view method. + */ +- (BOOL)isViewLoaded; + +/*! Override point for creating the view displaying this representation. This is called on your behalf the first time the \c -view method is called, so you would not want to call this explicitly; however this method must be overridden. This follows the "create" rule, and so it should return a retained view. +*/ +- (NSView *)createView NS_RETURNS_RETAINED; + +/*! Override point for initialization of view, after the HFRepresenter has the view set as its -view property. The default implementation does nothing. +*/ +- (void)initializeView; + +//@} + +/*! @name Accessing the HFController +*/ +//@{ +/*! Returns the HFController for the receiver. This is set by the controller from the call to \c addRepresenter:. A representer can only be in one controller at a time. */ +- (HFController *)controller; +//@} + +/*! @name Property change notifications +*/ +//@{ +/*! Indicates that the properties indicated by the given bits did change, and the view should be updated as to reflect the appropriate properties. This is the main mechanism by which representers are notified of changes to the controller. +*/ +- (void)controllerDidChange:(HFControllerPropertyBits)bits; +//@} + +/*! @name HFController convenience methods + Convenience covers for certain HFController methods +*/ +//@{ +/*! Equivalent to [[self controller] bytesPerLine] */ +- (NSUInteger)bytesPerLine; + +/*! Equivalent to [[self controller] bytesPerColumn] */ +- (NSUInteger)bytesPerColumn; + +/*! Equivalent to [[self controller] representer:self changedProperties:properties] . You may call this when some internal aspect of the receiver's view (such as its frame) has changed in a way that may globally change some property of the controller, and the controller should recalculate those properties. For example, the text representers call this with HFControllerDisplayedLineRange when the view grows vertically, because more data may be displayed. +*/ +- (void)representerChangedProperties:(HFControllerPropertyBits)properties; +//@} + +/*! @name Measurement + Methods related to measuring the HFRepresenter, so that it can be laid out properly by an HFLayoutController. All of these methods are candidates for overriding. +*/ +//@{ +/*! Returns the maximum number of bytes per line for the given view size. The default value is NSUIntegerMax, which means that the representer can display any number of lines for the given view size. */ +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth; + +/*! Returns the minimum view frame size for the given bytes per line. Default is to return 0, which means that the representer can display the given bytes per line in any view size. Fixed width views should return their fixed width. */ +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine; + +/*! Returns the maximum number of lines that could be displayed at once for a given view height. Default is to return DBL_MAX. */ +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight; +//@} + +/*! Returns the required byte granularity. HFLayoutRepresenter will constrain the bytes per line to a multiple of the granularity, e.g. so that UTF-16 characters are not split across lines. If different representers have different granularities, then it will constrain it to a multiple of all granularities, which may be very large. The default implementation returns 1. */ +- (NSUInteger)byteGranularity; + +/*! @name Auto-layout methods + Methods for simple auto-layout by HFLayoutRepresenter. See the HFLayoutRepresenter class for discussion of how it lays out representer views. +*/ +//@{ + + +/// The layout position for the receiver. +@property (nonatomic) NSPoint layoutPosition; + +/*! Returns the default layout position for representers of this class. Within the -init method, the view's layout position is set to the default for this class. You may override this to control the default layout position. See HFLayoutRepresenter for a discussion of the significance of the layout postition. +*/ ++ (NSPoint)defaultLayoutPosition; + +//@} + + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenter.m b/bsnes/gb/HexFiend/HFRepresenter.m new file mode 100644 index 00000000..510e3a0c --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenter.m @@ -0,0 +1,120 @@ +// +// HFRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import "HFRepresenter.h" + +@implementation HFRepresenter + +- (id)view { + if (! view) { + view = [self createView]; + [self initializeView]; + } + return view; +} + +- (BOOL)isViewLoaded { + return !! view; +} + +- (void)initializeView { + +} + +- (instancetype)init { + self = [super init]; + [self setLayoutPosition:[[self class] defaultLayoutPosition]]; + return self; +} + +- (void)dealloc { + [view release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [coder encodeObject:controller forKey:@"HFController"]; + [coder encodePoint:layoutPosition forKey:@"HFLayoutPosition"]; + [coder encodeObject:view forKey:@"HFRepresenterView"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super init]; + layoutPosition = [coder decodePointForKey:@"HFLayoutPosition"]; + controller = [coder decodeObjectForKey:@"HFController"]; // not retained + view = [[coder decodeObjectForKey:@"HFRepresenterView"] retain]; + return self; +} + +- (NSView *)createView { + UNIMPLEMENTED(); +} + +- (HFController *)controller { + return controller; +} + +- (void)_setController:(HFController *)val { + controller = val; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + USE(bits); +} + +- (NSUInteger)bytesPerLine { + HFASSERT([self controller] != nil); + return [[self controller] bytesPerLine]; +} + +- (NSUInteger)bytesPerColumn { + HFASSERT([self controller] != nil); + return [[self controller] bytesPerColumn]; +} + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth { + USE(viewWidth); + return NSUIntegerMax; +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + USE(bytesPerLine); + return 0; +} + +- (NSUInteger)byteGranularity { + return 1; +} + +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight { + USE(viewHeight); + return DBL_MAX; +} + +- (void)selectAll:sender { + [[self controller] selectAll:sender]; +} + +- (void)representerChangedProperties:(HFControllerPropertyBits)properties { + [[self controller] representer:self changedProperties:properties]; +} + +- (void)setLayoutPosition:(NSPoint)position { + layoutPosition = position; +} + +- (NSPoint)layoutPosition { + return layoutPosition; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(0, 0); +} + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterHexTextView.h b/bsnes/gb/HexFiend/HFRepresenterHexTextView.h new file mode 100644 index 00000000..8098846e --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterHexTextView.h @@ -0,0 +1,21 @@ +// +// HFRepresenterHexTextView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + + +@interface HFRepresenterHexTextView : HFRepresenterTextView { + CGGlyph glyphTable[17]; + CGFloat glyphAdvancement; + CGFloat spaceAdvancement; + + BOOL hidesNullBytes; +} + +@property(nonatomic) BOOL hidesNullBytes; + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterHexTextView.m b/bsnes/gb/HexFiend/HFRepresenterHexTextView.m new file mode 100644 index 00000000..6df41d80 --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterHexTextView.m @@ -0,0 +1,95 @@ +// +// HFRepresenterHexTextView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +@implementation HFRepresenterHexTextView + +- (void)generateGlyphTable { + const UniChar hexchars[17] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F',' '/* Plus a space char at the end for null bytes. */}; + _Static_assert(sizeof(CGGlyph[17]) == sizeof(glyphTable), "glyphTable is the wrong type"); + NSFont *font = [[self font] screenFont]; + + bool t = CTFontGetGlyphsForCharacters((CTFontRef)font, hexchars, glyphTable, 17); + HFASSERT(t); // We don't take kindly to strange fonts around here. + + CGFloat maxAdv = 0.0; + for(int i = 0; i < 17; i++) maxAdv = HFMax(maxAdv, [font advancementForGlyph:glyphTable[i]].width); + glyphAdvancement = maxAdv; + spaceAdvancement = maxAdv; +} + +- (void)setFont:(NSFont *)font { + [super setFont:font]; + [self generateGlyphTable]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + [self generateGlyphTable]; + return self; +} + +//no need for encodeWithCoder + +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount { + HFASSERT(bytes != NULL); + HFASSERT(glyphs != NULL); + HFASSERT(numBytes <= NSUIntegerMax); + HFASSERT(resultGlyphCount != NULL); + const NSUInteger bytesPerColumn = [self bytesPerColumn]; + NSUInteger glyphIndex = 0, byteIndex = 0; + NSUInteger remainingBytesInThisColumn = (bytesPerColumn ? bytesPerColumn - offsetIntoLine % bytesPerColumn : NSUIntegerMax); + CGFloat advanceBetweenColumns = [self advanceBetweenColumns]; + while (byteIndex < numBytes) { + unsigned char byte = bytes[byteIndex++]; + + CGFloat glyphAdvancementPlusAnySpace = glyphAdvancement; + if (--remainingBytesInThisColumn == 0) { + remainingBytesInThisColumn = bytesPerColumn; + glyphAdvancementPlusAnySpace += advanceBetweenColumns; + } + + BOOL useBlank = (hidesNullBytes && byte == 0); + advances[glyphIndex] = CGSizeMake(glyphAdvancement, 0); + glyphs[glyphIndex++] = (struct HFGlyph_t){.fontIndex = 0, .glyph = glyphTable[(useBlank? 16: byte >> 4)]}; + advances[glyphIndex] = CGSizeMake(glyphAdvancementPlusAnySpace, 0); + glyphs[glyphIndex++] = (struct HFGlyph_t){.fontIndex = 0, .glyph = glyphTable[(useBlank? 16: byte & 0xF)]}; + } + + *resultGlyphCount = glyphIndex; +} + +- (CGFloat)advancePerCharacter { + return 2 * glyphAdvancement; +} + +- (CGFloat)advanceBetweenColumns { + return glyphAdvancement; +} + +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount { + return 2 * byteCount; +} + +- (BOOL)hidesNullBytes { + return hidesNullBytes; +} + +- (void)setHidesNullBytes:(BOOL)flag +{ + flag = !! flag; + if (hidesNullBytes != flag) { + hidesNullBytes = flag; + [self setNeedsDisplay:YES]; + } +} + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterStringEncodingTextView.h b/bsnes/gb/HexFiend/HFRepresenterStringEncodingTextView.h new file mode 100644 index 00000000..2a87adae --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterStringEncodingTextView.h @@ -0,0 +1,37 @@ +// +// HFRepresenterStringEncodingTextView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +@interface HFRepresenterStringEncodingTextView : HFRepresenterTextView { + /* Tier 0 data (always up to date) */ + NSStringEncoding encoding; + uint8_t bytesPerChar; + + /* Tier 1 data (computed synchronously on-demand) */ + BOOL tier1DataIsStale; + struct HFGlyph_t replacementGlyph; + CGFloat glyphAdvancement; + + /* Tier 2 data (computed asynchronously on-demand) */ + struct HFGlyphTrie_t glyphTable; + + NSArray *fontCache; + + /* Background thread */ + OSSpinLock glyphLoadLock; + BOOL requestedCancel; + NSMutableArray *fonts; + NSMutableIndexSet *requestedCharacters; + NSOperationQueue *glyphLoader; +} + +/// Set and get the NSStringEncoding that is used +@property (nonatomic) NSStringEncoding encoding; + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterStringEncodingTextView.m b/bsnes/gb/HexFiend/HFRepresenterStringEncodingTextView.m new file mode 100644 index 00000000..fa8bcb1b --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterStringEncodingTextView.m @@ -0,0 +1,540 @@ +// +// HFRepresenterStringEncodingTextView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#include + +@implementation HFRepresenterStringEncodingTextView + +static NSString *copy1CharStringForByteValue(unsigned long long byteValue, NSUInteger bytesPerChar, NSStringEncoding encoding) { + NSString *result = nil; + unsigned char bytes[sizeof byteValue]; + /* If we are little endian, then the bytesPerChar doesn't matter, because it will all come out the same. If we are big endian, then it does matter. */ +#if ! __BIG_ENDIAN__ + *(unsigned long long *)bytes = byteValue; +#else + if (bytesPerChar == sizeof(uint8_t)) { + *(uint8_t *)bytes = (uint8_t)byteValue; + } else if (bytesPerChar == sizeof(uint16_t)) { + *(uint16_t *)bytes = (uint16_t)byteValue; + } else if (bytesPerChar == sizeof(uint32_t)) { + *(uint32_t *)bytes = (uint32_t)byteValue; + } else if (bytesPerChar == sizeof(uint64_t)) { + *(uint64_t *)bytes = (uint64_t)byteValue; + } else { + [NSException raise:NSInvalidArgumentException format:@"Unsupported bytesPerChar of %u", bytesPerChar]; + } +#endif + + /* ASCII is mishandled :( */ + BOOL encodingOK = YES; + if (encoding == NSASCIIStringEncoding && bytesPerChar == 1 && bytes[0] > 0x7F) { + encodingOK = NO; + } + + + + /* Now create a string from these bytes */ + if (encodingOK) { + result = [[NSString alloc] initWithBytes:bytes length:bytesPerChar encoding:encoding]; + + if ([result length] > 1) { + /* Try precomposing it */ + NSString *temp = [[result precomposedStringWithCompatibilityMapping] copy]; + [result release]; + result = temp; + } + + /* Ensure it has exactly one character */ + if ([result length] != 1) { + [result release]; + result = nil; + } + } + + /* All done */ + return result; +} + +static BOOL getGlyphs(CGGlyph *glyphs, NSString *string, NSFont *inputFont) { + NSUInteger length = [string length]; + HFASSERT(inputFont != nil); + NEW_ARRAY(UniChar, chars, length); + [string getCharacters:chars range:NSMakeRange(0, length)]; + bool result = CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars, glyphs, length); + /* A NO return means some or all characters were not mapped. This is OK. We'll use the replacement glyph. Unless we're calculating the replacement glyph! Hmm...maybe we should have a series of replacement glyphs that we try? */ + + //////////////////////// + // Workaround for a Mavericks bug. Still present as of 10.9.5 + // TODO: Hmm, still? Should look into this again, either it's not a bug or Apple needs a poke. + if(!result) for(NSUInteger i = 0; i < length; i+=15) { + CFIndex x = length-i; + if(x > 15) x = 15; + result = CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars+i, glyphs+i, x); + if(!result) break; + } + //////////////////////// + + FREE_ARRAY(chars); + return result; +} + +static void generateGlyphs(NSFont *baseFont, NSMutableArray *fonts, struct HFGlyph_t *outGlyphs, NSInteger bytesPerChar, NSStringEncoding encoding, const NSUInteger *charactersToLoad, NSUInteger charactersToLoadCount, CGFloat *outMaxAdvance) { + /* If the caller wants the advance, initialize it to 0 */ + if (outMaxAdvance) *outMaxAdvance = 0; + + /* Invalid glyph marker */ + const struct HFGlyph_t invalidGlyph = {.fontIndex = kHFGlyphFontIndexInvalid, .glyph = -1}; + + NSCharacterSet *coveredSet = [baseFont coveredCharacterSet]; + NSMutableString *coveredGlyphFetchingString = [[NSMutableString alloc] init]; + NSMutableIndexSet *coveredGlyphIndexes = [[NSMutableIndexSet alloc] init]; + NSMutableString *substitutionFontsGlyphFetchingString = [[NSMutableString alloc] init]; + NSMutableIndexSet *substitutionGlyphIndexes = [[NSMutableIndexSet alloc] init]; + + /* Loop over all the characters, appending them to our glyph fetching string */ + NSUInteger idx; + for (idx = 0; idx < charactersToLoadCount; idx++) { + NSString *string = copy1CharStringForByteValue(charactersToLoad[idx], bytesPerChar, encoding); + if (string == nil) { + /* This byte value is not represented in this char set (e.g. upper 128 in ASCII) */ + outGlyphs[idx] = invalidGlyph; + } else { + if ([coveredSet characterIsMember:[string characterAtIndex:0]]) { + /* It's covered by our base font */ + [coveredGlyphFetchingString appendString:string]; + [coveredGlyphIndexes addIndex:idx]; + } else { + /* Maybe there's a substitution font */ + [substitutionFontsGlyphFetchingString appendString:string]; + [substitutionGlyphIndexes addIndex:idx]; + } + } + [string release]; + } + + + /* Fetch the non-substitute glyphs */ + { + NEW_ARRAY(CGGlyph, cgglyphs, [coveredGlyphFetchingString length]); + BOOL success = getGlyphs(cgglyphs, coveredGlyphFetchingString, baseFont); + HFASSERT(success == YES); + NSUInteger numGlyphs = [coveredGlyphFetchingString length]; + + /* Fill in our glyphs array */ + NSUInteger coveredGlyphIdx = [coveredGlyphIndexes firstIndex]; + for (NSUInteger i=0; i < numGlyphs; i++) { + outGlyphs[coveredGlyphIdx] = (struct HFGlyph_t){.fontIndex = 0, .glyph = cgglyphs[i]}; + coveredGlyphIdx = [coveredGlyphIndexes indexGreaterThanIndex:coveredGlyphIdx]; + + /* Record the advancement. Note that this may be more efficient to do in bulk. */ + if (outMaxAdvance) *outMaxAdvance = HFMax(*outMaxAdvance, [baseFont advancementForGlyph:cgglyphs[i]].width); + + } + HFASSERT(coveredGlyphIdx == NSNotFound); //we must have exhausted the table + FREE_ARRAY(cgglyphs); + } + + /* Now do substitution glyphs. */ + { + NSUInteger substitutionGlyphIndex = [substitutionGlyphIndexes firstIndex], numSubstitutionChars = [substitutionFontsGlyphFetchingString length]; + for (NSUInteger i=0; i < numSubstitutionChars; i++) { + CTFontRef substitutionFont = CTFontCreateForString((CTFontRef)baseFont, (CFStringRef)substitutionFontsGlyphFetchingString, CFRangeMake(i, 1)); + if (substitutionFont) { + /* We have a font for this string */ + CGGlyph glyph; + unichar c = [substitutionFontsGlyphFetchingString characterAtIndex:i]; + NSString *substring = [[NSString alloc] initWithCharacters:&c length:1]; + BOOL success = getGlyphs(&glyph, substring, (NSFont *)substitutionFont); + [substring release]; + + if (! success) { + /* Turns out there wasn't a glyph like we thought there would be, so set an invalid glyph marker */ + outGlyphs[substitutionGlyphIndex] = invalidGlyph; + } else { + /* Find the index in fonts. If none, add to it. */ + HFASSERT(fonts != nil); + NSUInteger fontIndex = [fonts indexOfObject:(id)substitutionFont]; + if (fontIndex == NSNotFound) { + [fonts addObject:(id)substitutionFont]; + fontIndex = [fonts count] - 1; + } + + /* Now make the glyph */ + HFASSERT(fontIndex < UINT16_MAX); + outGlyphs[substitutionGlyphIndex] = (struct HFGlyph_t){.fontIndex = (uint16_t)fontIndex, .glyph = glyph}; + } + + /* We're done with this */ + CFRelease(substitutionFont); + + } + substitutionGlyphIndex = [substitutionGlyphIndexes indexGreaterThanIndex:substitutionGlyphIndex]; + } + } + + [coveredGlyphFetchingString release]; + [coveredGlyphIndexes release]; + [substitutionFontsGlyphFetchingString release]; + [substitutionGlyphIndexes release]; +} + +static int compareGlyphFontIndexes(const void *p1, const void *p2) { + const struct HFGlyph_t *g1 = p1, *g2 = p2; + if (g1->fontIndex != g2->fontIndex) { + /* Prefer to sort by font index */ + return (g1->fontIndex > g2->fontIndex) - (g2->fontIndex > g1->fontIndex); + } else { + /* If they have equal font indexes, sort by glyph value */ + return (g1->glyph > g2->glyph) - (g2->glyph > g1->glyph); + } +} + +- (void)threadedPrecacheGlyphs:(const struct HFGlyph_t *)glyphs withFonts:(NSArray *)localFonts count:(NSUInteger)count { + /* This method draws glyphs anywhere, so that they get cached by CG and drawing them a second time can be fast. */ + NSUInteger i, validGlyphCount; + + /* We can use 0 advances */ + NEW_ARRAY(CGSize, advances, count); + bzero(advances, count * sizeof *advances); + + /* Make a local copy of the glyphs, and sort them according to their font index so that we can draw them with the fewest runs. */ + NEW_ARRAY(struct HFGlyph_t, validGlyphs, count); + + validGlyphCount = 0; + for (i=0; i < count; i++) { + if (glyphs[i].glyph <= kCGGlyphMax && glyphs[i].fontIndex != kHFGlyphFontIndexInvalid) { + validGlyphs[validGlyphCount++] = glyphs[i]; + } + } + qsort(validGlyphs, validGlyphCount, sizeof *validGlyphs, compareGlyphFontIndexes); + + /* Remove duplicate glyphs */ + NSUInteger trailing = 0; + struct HFGlyph_t lastGlyph = {.glyph = kCGFontIndexInvalid, .fontIndex = kHFGlyphFontIndexInvalid}; + for (i=0; i < validGlyphCount; i++) { + if (! HFGlyphEqualsGlyph(lastGlyph, validGlyphs[i])) { + lastGlyph = validGlyphs[i]; + validGlyphs[trailing++] = lastGlyph; + } + } + validGlyphCount = trailing; + + /* Draw the glyphs in runs */ + NEW_ARRAY(CGGlyph, cgglyphs, count); + NSImage *glyphDrawingImage = [[NSImage alloc] initWithSize:NSMakeSize(100, 100)]; + [glyphDrawingImage lockFocus]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + HFGlyphFontIndex runFontIndex = -1; + NSUInteger runLength = 0; + for (i=0; i <= validGlyphCount; i++) { + if (i == validGlyphCount || validGlyphs[i].fontIndex != runFontIndex) { + /* End the current run */ + if (runLength > 0) { + NSLog(@"Drawing with %@", [localFonts[runFontIndex] screenFont]); + [[localFonts[runFontIndex] screenFont] set]; + CGContextSetTextPosition(ctx, 0, 50); + CGContextShowGlyphsWithAdvances(ctx, cgglyphs, advances, runLength); + } + NSLog(@"Drew a run of length %lu", (unsigned long)runLength); + runLength = 0; + if (i < validGlyphCount) runFontIndex = validGlyphs[i].fontIndex; + } + if (i < validGlyphCount) { + /* Append to the current run */ + cgglyphs[runLength++] = validGlyphs[i].glyph; + } + } + + /* All done */ + [glyphDrawingImage unlockFocus]; + [glyphDrawingImage release]; + FREE_ARRAY(advances); + FREE_ARRAY(validGlyphs); + FREE_ARRAY(cgglyphs); +} + +- (void)threadedLoadGlyphs:(id)unused { + /* Note that this is running on a background thread */ + USE(unused); + + /* Do some things under the lock. Someone else may wish to read fonts, and we're going to write to it, so make a local copy. Also figure out what characters to load. */ + NSMutableArray *localFonts; + NSIndexSet *charactersToLoad; + OSSpinLockLock(&glyphLoadLock); + localFonts = [fonts mutableCopy]; + charactersToLoad = requestedCharacters; + /* Set requestedCharacters to nil so that the caller knows we aren't going to check again, and will have to re-invoke us. */ + requestedCharacters = nil; + OSSpinLockUnlock(&glyphLoadLock); + + /* The base font is the first font */ + NSFont *font = localFonts[0]; + + NSUInteger charVal, glyphIdx, charCount = [charactersToLoad count]; + NEW_ARRAY(struct HFGlyph_t, glyphs, charCount); + + /* Now generate our glyphs */ + NEW_ARRAY(NSUInteger, characters, charCount); + [charactersToLoad getIndexes:characters maxCount:charCount inIndexRange:NULL]; + generateGlyphs(font, localFonts, glyphs, bytesPerChar, encoding, characters, charCount, NULL); + FREE_ARRAY(characters); + + /* The first time we draw glyphs, it's slow, so pre-cache them by drawing them now. */ + // This was disabled because it blows up the CG glyph cache + // [self threadedPrecacheGlyphs:glyphs withFonts:localFonts count:charCount]; + + /* Replace fonts. Do this before we insert into the glyph trie, because the glyph trie references fonts that we're just now putting in the fonts array. */ + id oldFonts; + OSSpinLockLock(&glyphLoadLock); + oldFonts = fonts; + fonts = localFonts; + OSSpinLockUnlock(&glyphLoadLock); + [oldFonts release]; + + /* Now insert all of the glyphs into the glyph trie */ + glyphIdx = 0; + for (charVal = [charactersToLoad firstIndex]; charVal != NSNotFound; charVal = [charactersToLoad indexGreaterThanIndex:charVal]) { + HFGlyphTrieInsert(&glyphTable, charVal, glyphs[glyphIdx++]); + } + FREE_ARRAY(glyphs); + + /* Trigger a redisplay */ + [self performSelectorOnMainThread:@selector(triggerRedisplay:) withObject:nil waitUntilDone:NO]; + + /* All done. We inherited the retain on requestedCharacters, so release it. */ + [charactersToLoad release]; +} + +- (void)triggerRedisplay:unused { + USE(unused); + [self setNeedsDisplay:YES]; +} + +- (void)beginLoadGlyphsForCharacters:(NSIndexSet *)charactersToLoad { + /* Create the operation (and maybe the operation queue itself) */ + if (! glyphLoader) { + glyphLoader = [[NSOperationQueue alloc] init]; + [glyphLoader setMaxConcurrentOperationCount:1]; + } + if (! fonts) { + NSFont *font = [self font]; + fonts = [[NSMutableArray alloc] initWithObjects:&font count:1]; + } + + BOOL needToStartOperation; + OSSpinLockLock(&glyphLoadLock); + if (requestedCharacters) { + /* There's a pending request, so just add to it */ + [requestedCharacters addIndexes:charactersToLoad]; + needToStartOperation = NO; + } else { + /* There's no pending request, so we will create one */ + requestedCharacters = [charactersToLoad mutableCopy]; + needToStartOperation = YES; + } + OSSpinLockUnlock(&glyphLoadLock); + + if (needToStartOperation) { + NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(threadedLoadGlyphs:) object:charactersToLoad]; + [glyphLoader addOperation:op]; + [op release]; + } +} + +- (void)dealloc { + HFGlyphTreeFree(&glyphTable); + [fonts release]; + [super dealloc]; +} + +- (void)staleTieredProperties { + tier1DataIsStale = YES; + /* We have to free the glyph table */ + requestedCancel = YES; + [glyphLoader waitUntilAllOperationsAreFinished]; + requestedCancel = NO; + HFGlyphTreeFree(&glyphTable); + HFGlyphTrieInitialize(&glyphTable, bytesPerChar); + [fonts release]; + fonts = nil; + [fontCache release]; + fontCache = nil; +} + +- (void)setFont:(NSFont *)font { + [self staleTieredProperties]; + /* fonts is preloaded with our one font */ + if (! fonts) fonts = [[NSMutableArray alloc] init]; + [fonts addObject:font]; + [super setFont:font]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + encoding = (NSStringEncoding)[coder decodeInt64ForKey:@"HFStringEncoding"]; + bytesPerChar = HFStringEncodingCharacterLength(encoding); + [self staleTieredProperties]; + return self; +} + +- (instancetype)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + encoding = NSMacOSRomanStringEncoding; + bytesPerChar = HFStringEncodingCharacterLength(encoding); + [self staleTieredProperties]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeInt64:encoding forKey:@"HFStringEncoding"]; +} + +- (NSStringEncoding)encoding { + return encoding; +} + +- (void)setEncoding:(NSStringEncoding)val { + if (encoding != val) { + /* Our glyph table is now stale. Call this first to ensure our background operation is complete. */ + [self staleTieredProperties]; + + /* Store the new encoding. */ + encoding = val; + + /* Compute bytes per character */ + bytesPerChar = HFStringEncodingCharacterLength(encoding); + HFASSERT(bytesPerChar > 0); + + /* Ensure the tree knows about the new bytes per character */ + HFGlyphTrieInitialize(&glyphTable, bytesPerChar); + + /* Redraw ourselves with our new glyphs */ + [self setNeedsDisplay:YES]; + } +} + +- (void)loadTier1Data { + NSFont *font = [self font]; + + /* Use the max advance as the glyph advance */ + glyphAdvancement = HFCeil([font maximumAdvancement].width); + + /* Generate replacementGlyph */ + CGGlyph glyph[1]; + BOOL foundReplacement = NO; + if (! foundReplacement) foundReplacement = getGlyphs(glyph, @".", font); + if (! foundReplacement) foundReplacement = getGlyphs(glyph, @"*", font); + if (! foundReplacement) foundReplacement = getGlyphs(glyph, @"!", font); + if (! foundReplacement) { + /* Really we should just fall back to another font in this case */ + [NSException raise:NSInternalInconsistencyException format:@"Unable to find replacement glyph for font %@", font]; + } + replacementGlyph.fontIndex = 0; + replacementGlyph.glyph = glyph[0]; + + /* We're no longer stale */ + tier1DataIsStale = NO; +} + +/* Override of base class method for font substitution */ +- (NSFont *)fontAtSubstitutionIndex:(uint16_t)idx { + HFASSERT(idx != kHFGlyphFontIndexInvalid); + if (idx >= [fontCache count]) { + /* Our font cache is out of date. Take the lock and update the cache. */ + NSArray *newFonts = nil; + OSSpinLockLock(&glyphLoadLock); + HFASSERT(idx < [fonts count]); + newFonts = [fonts copy]; + OSSpinLockUnlock(&glyphLoadLock); + + /* Store the new cache */ + [fontCache release]; + fontCache = newFonts; + + /* Now our cache should be up to date */ + HFASSERT(idx < [fontCache count]); + } + return fontCache[idx]; +} + +/* Override of base class method in case we are 16 bit */ +- (NSUInteger)bytesPerCharacter { + return bytesPerChar; +} + +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount { + HFASSERT(bytes != NULL); + HFASSERT(glyphs != NULL); + HFASSERT(resultGlyphCount != NULL); + HFASSERT(advances != NULL); + USE(offsetIntoLine); + + /* Ensure we have advance, etc. before trying to use it */ + if (tier1DataIsStale) [self loadTier1Data]; + + CGSize advance = CGSizeMake(glyphAdvancement, 0); + NSMutableIndexSet *charactersToLoad = nil; //note: in UTF-32 this may have to move to an NSSet + + const uint8_t localBytesPerChar = bytesPerChar; + NSUInteger charIndex, numChars = numBytes / localBytesPerChar, byteIndex = 0; + for (charIndex = 0; charIndex < numChars; charIndex++) { + NSUInteger character = -1; + if (localBytesPerChar == 1) { + character = *(const uint8_t *)(bytes + byteIndex); + } else if (localBytesPerChar == 2) { + character = *(const uint16_t *)(bytes + byteIndex); + } else if (localBytesPerChar == 4) { + character = *(const uint32_t *)(bytes + byteIndex); + } + + struct HFGlyph_t glyph = HFGlyphTrieGet(&glyphTable, character); + if (glyph.glyph == 0 && glyph.fontIndex == 0) { + /* Unloaded glyph, so load it */ + if (! charactersToLoad) charactersToLoad = [[NSMutableIndexSet alloc] init]; + [charactersToLoad addIndex:character]; + glyph = replacementGlyph; + } else if (glyph.glyph == (uint16_t)-1 && glyph.fontIndex == kHFGlyphFontIndexInvalid) { + /* Missing glyph, so ignore it */ + glyph = replacementGlyph; + } else { + /* Valid glyph */ + } + + HFASSERT(glyph.fontIndex != kHFGlyphFontIndexInvalid); + + advances[charIndex] = advance; + glyphs[charIndex] = glyph; + byteIndex += localBytesPerChar; + } + *resultGlyphCount = numChars; + + if (charactersToLoad) { + [self beginLoadGlyphsForCharacters:charactersToLoad]; + [charactersToLoad release]; + } +} + +- (CGFloat)advancePerCharacter { + /* The glyph advancement is determined by our glyph table */ + if (tier1DataIsStale) [self loadTier1Data]; + return glyphAdvancement; +} + +- (CGFloat)advanceBetweenColumns { + return 0; //don't have any space between columns +} + +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount { + return byteCount / [self bytesPerCharacter]; +} + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterTextView.h b/bsnes/gb/HexFiend/HFRepresenterTextView.h new file mode 100644 index 00000000..7e9edbb6 --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterTextView.h @@ -0,0 +1,146 @@ +// +// HFRepresenterTextView.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +/* Bytes per column philosophy + + _hftvflags.bytesPerColumn is the number of bytes that should be displayed consecutively, as one column. A space separates one column from the next. HexFiend 1.0 displayed 1 byte per column, and setting bytesPerColumn to 1 in this version reproduces that behavior. The vertical guidelines displayed by HexFiend 1.0 are only drawn when bytesPerColumn is set to 1. + + We use some number of bits to hold the number of bytes per column, so the highest value we can store is ((2 ^ numBits) - 1). We can't tell the user that the max is not a power of 2, so we pin the value to the highest representable power of 2, or (2 ^ (numBits - 1)). We allow integral values from 0 to the pinned maximum, inclusive; powers of 2 are not required. The setter method uses HFTV_BYTES_PER_COLUMN_MAX_VALUE to stay within the representable range. + + Since a value of zero is nonsensical, we can use it to specify no spaces at all. +*/ + +#define HFTV_BYTES_PER_COLUMN_MAX_VALUE (1 << (HFTV_BYTES_PER_COLUMN_BITFIELD_SIZE - 1)) + +@class HFTextRepresenter; + + +/* The base class for HFTextRepresenter views - such as the hex or ASCII text view */ +@interface HFRepresenterTextView : NSView { +@private; + HFTextRepresenter *representer; + NSArray *cachedSelectedRanges; + CGFloat verticalOffset; + CGFloat horizontalContainerInset; + CGFloat defaultLineHeight; + NSTimer *caretTimer; + NSWindow *pulseWindow; + NSRect pulseWindowBaseFrameInScreenCoordinates; + NSRect lastDrawnCaretRect; + NSRect caretRectToDraw; + NSUInteger bytesBetweenVerticalGuides; + NSUInteger startingLineBackgroundColorIndex; + NSArray *rowBackgroundColors; + NSMutableDictionary *callouts; + + void (^byteColoring)(uint8_t byte, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a); + + struct { + unsigned antialias:1; + unsigned drawCallouts:1; + unsigned editable:1; + unsigned caretVisible:1; + unsigned registeredForAppNotifications:1; + unsigned withinMouseDown:1; + unsigned receivedMouseUp:1; + } _hftvflags; +} + +- (instancetype)initWithRepresenter:(HFTextRepresenter *)rep; +- (void)clearRepresenter; + +- (HFTextRepresenter *)representer; + +@property (nonatomic, copy) NSFont *font; + +/* Set and get data. setData: will invalidate the correct regions (perhaps none) */ +@property (nonatomic, copy) NSData *data; +@property (nonatomic) CGFloat verticalOffset; +@property (nonatomic) NSUInteger startingLineBackgroundColorIndex; +@property (nonatomic, getter=isEditable) BOOL editable; +@property (nonatomic, copy) NSArray *styles; +@property (nonatomic) BOOL shouldAntialias; + +- (BOOL)behavesAsTextField; +- (BOOL)showsFocusRing; +- (BOOL)isWithinMouseDown; + +- (NSRect)caretRect; + +@property (nonatomic) BOOL shouldDrawCallouts; + +- (void)setByteColoring:(void (^)(uint8_t byte, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a))coloring; + +- (NSPoint)originForCharacterAtByteIndex:(NSInteger)index; +- (NSUInteger)indexOfCharacterAtPoint:(NSPoint)point; + +/* The amount of padding space to inset from the left and right side. */ +@property (nonatomic) CGFloat horizontalContainerInset; + +/* The number of bytes between vertical guides. 0 means no drawing of guides. */ +@property (nonatomic) NSUInteger bytesBetweenVerticalGuides; + +/* To be invoked from drawRect:. */ +- (void)drawCaretIfNecessaryWithClip:(NSRect)clipRect; +- (void)drawSelectionIfNecessaryWithClip:(NSRect)clipRect; + +/* For font substitution. An index of 0 means the default (base) font. */ +- (NSFont *)fontAtSubstitutionIndex:(uint16_t)idx; + +/* Uniformly "rounds" the byte range so that it contains an integer number of characters. The algorithm is to "floor:" any character intersecting the min of the range are included, and any character extending beyond the end of the range is excluded. If both the min and the max are within a single character, then an empty range is returned. */ +- (NSRange)roundPartialByteRange:(NSRange)byteRange; + +- (void)drawTextWithClip:(NSRect)clipRect restrictingToTextInRanges:(NSArray *)restrictingToRanges; + +/* Must be overridden */ +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount; + +- (void)extractGlyphsForBytes:(const unsigned char *)bytePtr range:(NSRange)byteRange intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances withInclusionRanges:(NSArray *)restrictingToRanges initialTextOffset:(CGFloat *)initialTextOffset resultingGlyphCount:(NSUInteger *)resultingGlyphCount; + +/* Must be overridden - returns the max number of glyphs for a given number of bytes */ +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount; + +- (void)updateSelectedRanges; +- (void)terminateSelectionPulse; // Start fading the pulse. + +/* Given a rect edge, return an NSRect representing the maximum edge in that direction. The dimension in the direction of the edge is 0 (so if edge is NSMaxXEdge, the resulting width is 0). The returned rect is in the coordinate space of the receiver's view. If the byte range is not displayed, returns NSZeroRect. + */ +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forRange:(NSRange)range; + +/* The background color for the line at the given index. You may override this to return different colors. You may return nil to draw no color in this line (and then the empty space color will appear) */ +- (NSColor *)backgroundColorForLine:(NSUInteger)line; +- (NSColor *)backgroundColorForEmptySpace; + +/* Defaults to 1, may override */ +- (NSUInteger)bytesPerCharacter; + +/* Cover method for [[self representer] bytesPerLine] and [[self representer] bytesPerColumn] */ +- (NSUInteger)bytesPerLine; +- (NSUInteger)bytesPerColumn; + +- (CGFloat)lineHeight; + +/* Following two must be overridden */ +- (CGFloat)advanceBetweenColumns; +- (CGFloat)advancePerCharacter; + +- (CGFloat)advancePerColumn; +- (CGFloat)totalAdvanceForBytesInRange:(NSRange)range; + +/* Returns the number of lines that could be shown in this view at its given height (expressed in its local coordinate space) */ +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight; + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth; +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine; + +- (IBAction)selectAll:sender; + + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterTextView.m b/bsnes/gb/HexFiend/HFRepresenterTextView.m new file mode 100644 index 00000000..7fcbd0c7 --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterTextView.m @@ -0,0 +1,1766 @@ +// +// HFRepresenterTextView.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import +#import +#import + +static const NSTimeInterval HFCaretBlinkFrequency = 0.56; + +@implementation HFRepresenterTextView + +- (NSUInteger)_getGlyphs:(CGGlyph *)glyphs forString:(NSString *)string font:(NSFont *)inputFont { + NSUInteger length = [string length]; + UniChar chars[256]; + HFASSERT(length <= sizeof chars / sizeof *chars); + HFASSERT(inputFont != nil); + [string getCharacters:chars range:NSMakeRange(0, length)]; + if (! CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars, glyphs, length)) { + /* Some or all characters were not mapped. This is OK. We'll use the replacement glyph. */ + } + return length; +} + +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingLayoutManager:(NSLayoutManager *)layoutManager glyphs:(CGGlyph *)glyphs { + HFASSERT(layoutManager != NULL); + HFASSERT(string != NULL); + NSGlyph nsglyphs[GLYPH_BUFFER_SIZE]; + [[[layoutManager textStorage] mutableString] setString:string]; + NSUInteger glyphIndex, glyphCount = [layoutManager getGlyphs:nsglyphs range:NSMakeRange(0, MIN(GLYPH_BUFFER_SIZE, [layoutManager numberOfGlyphs]))]; + if (glyphs != NULL) { + /* Convert from unsigned int NSGlyphs to unsigned short CGGlyphs */ + for (glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) { + /* Get rid of NSControlGlyph */ + NSGlyph modifiedGlyph = nsglyphs[glyphIndex] == NSControlGlyph ? NSNullGlyph : nsglyphs[glyphIndex]; + HFASSERT(modifiedGlyph <= USHRT_MAX); + glyphs[glyphIndex] = (CGGlyph)modifiedGlyph; + } + } + return glyphCount; +} + +/* Returns the number of glyphs for the given string, using the given text view, and generating the glyphs if the glyphs parameter is not NULL */ +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingTextView:(NSTextView *)textView glyphs:(CGGlyph *)glyphs { + HFASSERT(string != NULL); + HFASSERT(textView != NULL); + [textView setString:string]; + [textView setNeedsDisplay:YES]; //ligature generation doesn't seem to happen without this, for some reason. This seems very fragile! We should find a better way to get this ligature information!! + return [self _glyphsForString:string withGeneratingLayoutManager:[textView layoutManager] glyphs:glyphs]; +} + +- (NSArray *)displayedSelectedContentsRanges { + if (! cachedSelectedRanges) { + cachedSelectedRanges = [[[self representer] displayedSelectedContentsRanges] copy]; + } + return cachedSelectedRanges; +} + +- (BOOL)_shouldHaveCaretTimer { + NSWindow *window = [self window]; + if (window == NULL) return NO; + if (! [window isKeyWindow]) return NO; + if (self != [window firstResponder]) return NO; + if (! _hftvflags.editable) return NO; + NSArray *ranges = [self displayedSelectedContentsRanges]; + if ([ranges count] != 1) return NO; + NSRange range = [ranges[0] rangeValue]; + if (range.length != 0) return NO; + return YES; +} + +- (NSUInteger)_effectiveBytesPerColumn { + /* returns the bytesPerColumn, unless it's larger than the bytes per character, in which case it returns 0 */ + NSUInteger bytesPerColumn = [self bytesPerColumn], bytesPerCharacter = [self bytesPerCharacter]; + return bytesPerColumn >= bytesPerCharacter ? bytesPerColumn : 0; +} + +// note: index may be negative +- (NSPoint)originForCharacterAtByteIndex:(NSInteger)index { + NSPoint result; + NSInteger bytesPerLine = (NSInteger)[self bytesPerLine]; + + // We want a nonnegative remainder + NSInteger lineIndex = index / bytesPerLine; + NSInteger byteIndexIntoLine = index % bytesPerLine; + while (byteIndexIntoLine < 0) { + byteIndexIntoLine += bytesPerLine; + lineIndex--; + } + + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + NSUInteger numConsumedColumns = (bytesPerColumn ? byteIndexIntoLine / bytesPerColumn : 0); + NSUInteger characterIndexIntoLine = byteIndexIntoLine / [self bytesPerCharacter]; + + result.x = [self horizontalContainerInset] + characterIndexIntoLine * [self advancePerCharacter] + numConsumedColumns * [self advanceBetweenColumns]; + result.y = (lineIndex - [self verticalOffset]) * [self lineHeight]; + + return result; +} + +- (NSUInteger)indexOfCharacterAtPoint:(NSPoint)point { + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + HFASSERT(bytesPerLine % bytesPerCharacter == 0); + CGFloat advancePerCharacter = [self advancePerCharacter]; + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + CGFloat floatRow = (CGFloat)floor([self verticalOffset] + point.y / [self lineHeight]); + NSUInteger byteIndexWithinRow; + + // to compute the column, we need to solve for byteIndexIntoLine in something like this: point.x = [self advancePerCharacter] * charIndexIntoLine + [self spaceBetweenColumns] * floor(byteIndexIntoLine / [self bytesPerColumn]). Start by computing the column (or if bytesPerColumn is 0, we don't have columns) + CGFloat insetX = point.x - [self horizontalContainerInset]; + if (insetX < 0) { + //handle the case of dragging within the container inset + byteIndexWithinRow = 0; + } + else if (bytesPerColumn == 0) { + /* We don't have columns */ + byteIndexWithinRow = bytesPerCharacter * (NSUInteger)(insetX / advancePerCharacter); + } + else { + CGFloat advancePerColumn = [self advancePerColumn]; + HFASSERT(advancePerColumn > 0); + CGFloat floatColumn = insetX / advancePerColumn; + HFASSERT(floatColumn >= 0 && floatColumn <= NSUIntegerMax); + CGFloat startOfColumn = advancePerColumn * HFFloor(floatColumn); + HFASSERT(startOfColumn <= insetX); + CGFloat xOffsetWithinColumn = insetX - startOfColumn; + CGFloat charIndexWithinColumn = xOffsetWithinColumn / advancePerCharacter; //charIndexWithinColumn may be larger than bytesPerColumn if the user clicked on the space between columns + HFASSERT(charIndexWithinColumn >= 0 && charIndexWithinColumn <= NSUIntegerMax / bytesPerCharacter); + NSUInteger byteIndexWithinColumn = bytesPerCharacter * (NSUInteger)charIndexWithinColumn; + byteIndexWithinRow = bytesPerColumn * (NSUInteger)floatColumn + byteIndexWithinColumn; //this may trigger overflow to the next column, but that's OK + byteIndexWithinRow = MIN(byteIndexWithinRow, bytesPerLine); //don't let clicking to the right of the line overflow to the next line + } + HFASSERT(floatRow >= 0 && floatRow <= NSUIntegerMax); + NSUInteger row = (NSUInteger)floatRow; + return (row * bytesPerLine + byteIndexWithinRow) / bytesPerCharacter; +} + +- (NSRect)caretRect { + NSArray *ranges = [self displayedSelectedContentsRanges]; + HFASSERT([ranges count] == 1); + NSRange range = [ranges[0] rangeValue]; + HFASSERT(range.length == 0); + + NSPoint caretBaseline = [self originForCharacterAtByteIndex:range.location]; + return NSMakeRect(caretBaseline.x - 1, caretBaseline.y, 1, [self lineHeight]); +} + +- (void)_blinkCaret:(NSTimer *)timer { + HFASSERT(timer == caretTimer); + if (_hftvflags.caretVisible) { + _hftvflags.caretVisible = NO; + [self setNeedsDisplayInRect:lastDrawnCaretRect]; + caretRectToDraw = NSZeroRect; + } + else { + _hftvflags.caretVisible = YES; + caretRectToDraw = [self caretRect]; + [self setNeedsDisplayInRect:caretRectToDraw]; + } +} + +- (void)_updateCaretTimerWithFirstResponderStatus:(BOOL)treatAsHavingFirstResponder { + BOOL hasCaretTimer = !! caretTimer; + BOOL shouldHaveCaretTimer = treatAsHavingFirstResponder && [self _shouldHaveCaretTimer]; + if (shouldHaveCaretTimer == YES && hasCaretTimer == NO) { + caretTimer = [[NSTimer timerWithTimeInterval:HFCaretBlinkFrequency target:self selector:@selector(_blinkCaret:) userInfo:nil repeats:YES] retain]; + NSRunLoop *loop = [NSRunLoop currentRunLoop]; + [loop addTimer:caretTimer forMode:NSDefaultRunLoopMode]; + [loop addTimer:caretTimer forMode:NSModalPanelRunLoopMode]; + if ([self enclosingMenuItem] != NULL) { + [loop addTimer:caretTimer forMode:NSEventTrackingRunLoopMode]; + } + } + else if (shouldHaveCaretTimer == NO && hasCaretTimer == YES) { + [caretTimer invalidate]; + [caretTimer release]; + caretTimer = nil; + caretRectToDraw = NSZeroRect; + if (! NSIsEmptyRect(lastDrawnCaretRect)) { + [self setNeedsDisplayInRect:lastDrawnCaretRect]; + } + } + HFASSERT(shouldHaveCaretTimer == !! caretTimer); +} + +- (void)_updateCaretTimer { + [self _updateCaretTimerWithFirstResponderStatus: self == [[self window] firstResponder]]; +} + +/* When you click or type, the caret appears immediately - do that here */ +- (void)_forceCaretOnIfHasCaretTimer { + if (caretTimer) { + [caretTimer invalidate]; + [caretTimer release]; + caretTimer = nil; + [self _updateCaretTimer]; + + _hftvflags.caretVisible = YES; + caretRectToDraw = [self caretRect]; + [self setNeedsDisplayInRect:caretRectToDraw]; + } +} + +/* Returns the range of lines containing the selected contents ranges (as NSValues containing NSRanges), or {NSNotFound, 0} if ranges is nil or empty */ +- (NSRange)_lineRangeForContentsRanges:(NSArray *)ranges { + NSUInteger minLine = NSUIntegerMax; + NSUInteger maxLine = 0; + NSUInteger bytesPerLine = [self bytesPerLine]; + FOREACH(NSValue *, rangeValue, ranges) { + NSRange range = [rangeValue rangeValue]; + if (range.length > 0) { + NSUInteger lineForRangeStart = range.location / bytesPerLine; + NSUInteger lineForRangeEnd = NSMaxRange(range) / bytesPerLine; + HFASSERT(lineForRangeStart <= lineForRangeEnd); + minLine = MIN(minLine, lineForRangeStart); + maxLine = MAX(maxLine, lineForRangeEnd); + } + } + if (minLine > maxLine) return NSMakeRange(NSNotFound, 0); + else return NSMakeRange(minLine, maxLine - minLine + 1); +} + +- (NSRect)_rectForLineRange:(NSRange)lineRange { + HFASSERT(lineRange.location != NSNotFound); + NSUInteger bytesPerLine = [self bytesPerLine]; + NSRect bounds = [self bounds]; + NSRect result; + result.origin.x = NSMinX(bounds); + result.size.width = NSWidth(bounds); + result.origin.y = [self originForCharacterAtByteIndex:lineRange.location * bytesPerLine].y; + result.size.height = [self lineHeight] * lineRange.length; + return result; +} + +static int range_compare(const void *ap, const void *bp) { + const NSRange *a = ap; + const NSRange *b = bp; + if (a->location < b->location) return -1; + if (a->location > b->location) return 1; + if (a->length < b->length) return -1; + if (a->length > b->length) return 1; + return 0; +} + +enum LineCoverage_t { + eCoverageNone, + eCoveragePartial, + eCoverageFull +}; + +- (void)_linesWithParityChangesFromRanges:(const NSRange *)oldRanges count:(NSUInteger)oldRangeCount toRanges:(const NSRange *)newRanges count:(NSUInteger)newRangeCount intoIndexSet:(NSMutableIndexSet *)result { + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger oldParity=0, newParity=0; + NSUInteger oldRangeIndex = 0, newRangeIndex = 0; + NSUInteger currentCharacterIndex = MIN(oldRanges[oldRangeIndex].location, newRanges[newRangeIndex].location); + oldParity = (currentCharacterIndex >= oldRanges[oldRangeIndex].location); + newParity = (currentCharacterIndex >= newRanges[newRangeIndex].location); + // NSLog(@"Old %s, new %s at %u (%u, %u)", oldParity ? "on" : "off", newParity ? "on" : "off", currentCharacterIndex, oldRanges[oldRangeIndex].location, newRanges[newRangeIndex].location); + for (;;) { + NSUInteger oldDivision = NSUIntegerMax, newDivision = NSUIntegerMax; + /* Move up to the next parity change */ + if (oldRangeIndex < oldRangeCount) { + const NSRange oldRange = oldRanges[oldRangeIndex]; + oldDivision = oldRange.location + (oldParity ? oldRange.length : 0); + } + if (newRangeIndex < newRangeCount) { + const NSRange newRange = newRanges[newRangeIndex]; + newDivision = newRange.location + (newParity ? newRange.length : 0); + } + + NSUInteger division = MIN(oldDivision, newDivision); + HFASSERT(division > currentCharacterIndex); + + // NSLog(@"Division %u", division); + + if (division == NSUIntegerMax) break; + + if (oldParity != newParity) { + /* The parities did not match through this entire range, so add all intersected lines to the result index set */ + NSUInteger startLine = currentCharacterIndex / bytesPerLine; + NSUInteger endLine = HFDivideULRoundingUp(division, bytesPerLine); + HFASSERT(endLine >= startLine); + // NSLog(@"Adding lines %u -> %u", startLine, endLine); + [result addIndexesInRange:NSMakeRange(startLine, endLine - startLine)]; + } + if (division == oldDivision) { + oldRangeIndex += oldParity; + oldParity = ! oldParity; + // NSLog(@"Old range switching %s at %u", oldParity ? "on" : "off", division); + } + if (division == newDivision) { + newRangeIndex += newParity; + newParity = ! newParity; + // NSLog(@"New range switching %s at %u", newParity ? "on" : "off", division); + } + currentCharacterIndex = division; + } +} + +- (void)_addLinesFromRanges:(const NSRange *)ranges count:(NSUInteger)count toIndexSet:(NSMutableIndexSet *)set { + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger i; + for (i=0; i < count; i++) { + NSUInteger firstLine = ranges[i].location / bytesPerLine; + NSUInteger lastLine = HFDivideULRoundingUp(NSMaxRange(ranges[i]), bytesPerLine); + [set addIndexesInRange:NSMakeRange(firstLine, lastLine - firstLine)]; + } +} + +- (NSIndexSet *)_indexSetOfLinesNeedingRedrawWhenChangingSelectionFromRanges:(NSArray *)oldSelectedRangeArray toRanges:(NSArray *)newSelectedRangeArray { + NSUInteger oldRangeCount = 0, newRangeCount = 0; + + NEW_ARRAY(NSRange, oldRanges, [oldSelectedRangeArray count]); + NEW_ARRAY(NSRange, newRanges, [newSelectedRangeArray count]); + + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + + /* Extract all the ranges into a local array */ + FOREACH(NSValue *, rangeValue1, oldSelectedRangeArray) { + NSRange range = [rangeValue1 rangeValue]; + if (range.length > 0) { + oldRanges[oldRangeCount++] = range; + } + } + FOREACH(NSValue *, rangeValue2, newSelectedRangeArray) { + NSRange range = [rangeValue2 rangeValue]; + if (range.length > 0) { + newRanges[newRangeCount++] = range; + } + } + +#if ! NDEBUG + /* Assert that ranges of arrays do not have any self-intersection; this is supposed to be enforced by our HFController. Also assert that they aren't "just touching"; if they are they should be merged into a single range. */ + for (NSUInteger i=0; i < oldRangeCount; i++) { + for (NSUInteger j=i+1; j < oldRangeCount; j++) { + HFASSERT(NSIntersectionRange(oldRanges[i], oldRanges[j]).length == 0); + HFASSERT(NSMaxRange(oldRanges[i]) != oldRanges[j].location && NSMaxRange(oldRanges[j]) != oldRanges[i].location); + } + } + for (NSUInteger i=0; i < newRangeCount; i++) { + for (NSUInteger j=i+1; j < newRangeCount; j++) { + HFASSERT(NSIntersectionRange(newRanges[i], newRanges[j]).length == 0); + HFASSERT(NSMaxRange(newRanges[i]) != newRanges[j].location && NSMaxRange(newRanges[j]) != newRanges[i].location); + } + } +#endif + + if (newRangeCount == 0) { + [self _addLinesFromRanges:oldRanges count:oldRangeCount toIndexSet:result]; + } + else if (oldRangeCount == 0) { + [self _addLinesFromRanges:newRanges count:newRangeCount toIndexSet:result]; + } + else { + /* Sort the arrays, since _linesWithParityChangesFromRanges needs it */ + qsort(oldRanges, oldRangeCount, sizeof *oldRanges, range_compare); + qsort(newRanges, newRangeCount, sizeof *newRanges, range_compare); + + [self _linesWithParityChangesFromRanges:oldRanges count:oldRangeCount toRanges:newRanges count:newRangeCount intoIndexSet:result]; + } + + FREE_ARRAY(oldRanges); + FREE_ARRAY(newRanges); + + return result; +} + +- (void)updateSelectedRanges { + NSArray *oldSelectedRanges = cachedSelectedRanges; + cachedSelectedRanges = [[[self representer] displayedSelectedContentsRanges] copy]; + NSIndexSet *indexSet = [self _indexSetOfLinesNeedingRedrawWhenChangingSelectionFromRanges:oldSelectedRanges toRanges:cachedSelectedRanges]; + BOOL lastCaretRectNeedsRedraw = ! NSIsEmptyRect(lastDrawnCaretRect); + NSRange lineRangeToInvalidate = NSMakeRange(NSUIntegerMax, 0); + for (NSUInteger lineIndex = [indexSet firstIndex]; ; lineIndex = [indexSet indexGreaterThanIndex:lineIndex]) { + if (lineIndex != NSNotFound && NSMaxRange(lineRangeToInvalidate) == lineIndex) { + lineRangeToInvalidate.length++; + } + else { + if (lineRangeToInvalidate.length > 0) { + NSRect rectToInvalidate = [self _rectForLineRange:lineRangeToInvalidate]; + [self setNeedsDisplayInRect:rectToInvalidate]; + lastCaretRectNeedsRedraw = lastCaretRectNeedsRedraw && ! NSContainsRect(rectToInvalidate, lastDrawnCaretRect); + } + lineRangeToInvalidate = NSMakeRange(lineIndex, 1); + } + if (lineIndex == NSNotFound) break; + } + + if (lastCaretRectNeedsRedraw) [self setNeedsDisplayInRect:lastDrawnCaretRect]; + [oldSelectedRanges release]; //balance the retain we borrowed from the ivar + [self _updateCaretTimer]; + [self _forceCaretOnIfHasCaretTimer]; + + // A new pulse window will be created at the new selected range if necessary. + [self terminateSelectionPulse]; +} + +- (void)drawPulseBackgroundInRect:(NSRect)pulseRect { + [[NSColor yellowColor] set]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + CGContextSaveGState(ctx); + [[NSBezierPath bezierPathWithRoundedRect:pulseRect xRadius:25 yRadius:25] addClip]; + NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[NSColor yellowColor] endingColor:[NSColor colorWithCalibratedRed:(CGFloat)1. green:(CGFloat).75 blue:0 alpha:1]]; + [gradient drawInRect:pulseRect angle:90]; + [gradient release]; + CGContextRestoreGState(ctx); +} + +- (void)fadePulseWindowTimer:(NSTimer *)timer { + // TODO: close & invalidate immediatley if view scrolls. + NSWindow *window = [timer userInfo]; + CGFloat alpha = [window alphaValue]; + alpha -= (CGFloat)(3. / 30.); + if (alpha < 0) { + [window close]; + [timer invalidate]; + } + else { + [window setAlphaValue:alpha]; + } +} + +- (void)terminateSelectionPulse { + if (pulseWindow) { + [[self window] removeChildWindow:pulseWindow]; + [pulseWindow setFrame:pulseWindowBaseFrameInScreenCoordinates display:YES animate:NO]; + [NSTimer scheduledTimerWithTimeInterval:1. / 30. target:self selector:@selector(fadePulseWindowTimer:) userInfo:pulseWindow repeats:YES]; + //release is not necessary, since it relases when closed by default + pulseWindow = nil; + pulseWindowBaseFrameInScreenCoordinates = NSZeroRect; + } +} + +- (void)drawCaretIfNecessaryWithClip:(NSRect)clipRect { + NSRect caretRect = NSIntersectionRect(caretRectToDraw, clipRect); + if (! NSIsEmptyRect(caretRect)) { + [[NSColor controlTextColor] set]; + NSRectFill(caretRect); + lastDrawnCaretRect = caretRect; + } + if (NSIsEmptyRect(caretRectToDraw)) lastDrawnCaretRect = NSZeroRect; +} + + +/* This is the color when we are the first responder in the key window */ +- (NSColor *)primaryTextSelectionColor { + return [NSColor selectedTextBackgroundColor]; +} + +/* This is the color when we are not in the key window */ +- (NSColor *)inactiveTextSelectionColor { + if (@available(macOS 10.14, *)) { + return [NSColor unemphasizedSelectedTextBackgroundColor]; + } + return [NSColor colorWithCalibratedWhite: (CGFloat)(212./255.) alpha:1]; +} + +/* This is the color when we are not the first responder, but we are in the key window */ +- (NSColor *)secondaryTextSelectionColor { + if (@available(macOS 10.14, *)) { + return [NSColor unemphasizedSelectedTextBackgroundColor]; + } + return [NSColor colorWithCalibratedWhite: (CGFloat)(212./255.) alpha:1]; +} + +- (NSColor *)textSelectionColor { + NSWindow *window = [self window]; + if (window == nil) return [self primaryTextSelectionColor]; + else if (! [window isKeyWindow]) return [self inactiveTextSelectionColor]; + else if (self != [window firstResponder]) return [self secondaryTextSelectionColor]; + else return [self primaryTextSelectionColor]; +} + +- (void)drawSelectionIfNecessaryWithClip:(NSRect)clipRect { + NSArray *ranges = [self displayedSelectedContentsRanges]; + NSUInteger bytesPerLine = [self bytesPerLine]; + [[self textSelectionColor] set]; + CGFloat lineHeight = [self lineHeight]; + FOREACH(NSValue *, rangeValue, ranges) { + NSRange range = [rangeValue rangeValue]; + if (range.length > 0) { + NSUInteger startByteIndex = range.location; + NSUInteger endByteIndexForThisRange = range.location + range.length - 1; + NSUInteger byteIndex = startByteIndex; + while (byteIndex <= endByteIndexForThisRange) { + NSUInteger endByteIndexForLine = ((byteIndex / bytesPerLine) + 1) * bytesPerLine - 1; + NSUInteger endByteForThisLineOfRange = MIN(endByteIndexForThisRange, endByteIndexForLine); + NSPoint startPoint = [self originForCharacterAtByteIndex:byteIndex]; + NSPoint endPoint = [self originForCharacterAtByteIndex:endByteForThisLineOfRange]; + NSRect selectionRect = NSMakeRect(startPoint.x, startPoint.y, endPoint.x + [self advancePerCharacter] - startPoint.x, lineHeight); + NSRect clippedSelectionRect = NSIntersectionRect(selectionRect, clipRect); + if (! NSIsEmptyRect(clippedSelectionRect)) { + NSRectFill(clippedSelectionRect); + } + byteIndex = endByteForThisLineOfRange + 1; + } + } + } +} + +- (BOOL)acceptsFirstResponder { + return YES; +} + +- (BOOL)hasVisibleDisplayedSelectedContentsRange { + FOREACH(NSValue *, rangeValue, [self displayedSelectedContentsRanges]) { + NSRange range = [rangeValue rangeValue]; + if (range.length > 0) { + return YES; + } + } + return NO; +} + +- (BOOL)becomeFirstResponder { + BOOL result = [super becomeFirstResponder]; + [self _updateCaretTimerWithFirstResponderStatus:YES]; + if ([self showsFocusRing] || [self hasVisibleDisplayedSelectedContentsRange]) { + [self setNeedsDisplay:YES]; + } + return result; +} + +- (BOOL)resignFirstResponder { + BOOL result = [super resignFirstResponder]; + [self _updateCaretTimerWithFirstResponderStatus:NO]; + BOOL needsRedisplay = NO; + if ([self showsFocusRing]) needsRedisplay = YES; + else if (! NSIsEmptyRect(lastDrawnCaretRect)) needsRedisplay = YES; + else if ([self hasVisibleDisplayedSelectedContentsRange]) needsRedisplay = YES; + if (needsRedisplay) [self setNeedsDisplay:YES]; + return result; +} + +- (instancetype)initWithRepresenter:(HFTextRepresenter *)rep { + self = [super initWithFrame:NSMakeRect(0, 0, 1, 1)]; + horizontalContainerInset = 4; + representer = rep; + _hftvflags.editable = YES; + + return self; +} + +- (void)clearRepresenter { + representer = nil; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeObject:representer forKey:@"HFRepresenter"]; + [coder encodeObject:_font forKey:@"HFFont"]; + [coder encodeObject:_data forKey:@"HFData"]; + [coder encodeDouble:verticalOffset forKey:@"HFVerticalOffset"]; + [coder encodeDouble:horizontalContainerInset forKey:@"HFHorizontalContainerOffset"]; + [coder encodeDouble:defaultLineHeight forKey:@"HFDefaultLineHeight"]; + [coder encodeInt64:bytesBetweenVerticalGuides forKey:@"HFBytesBetweenVerticalGuides"]; + [coder encodeInt64:startingLineBackgroundColorIndex forKey:@"HFStartingLineBackgroundColorIndex"]; + [coder encodeObject:rowBackgroundColors forKey:@"HFRowBackgroundColors"]; + [coder encodeBool:_hftvflags.antialias forKey:@"HFAntialias"]; + [coder encodeBool:_hftvflags.drawCallouts forKey:@"HFDrawCallouts"]; + [coder encodeBool:_hftvflags.editable forKey:@"HFEditable"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + representer = [coder decodeObjectForKey:@"HFRepresenter"]; + _font = [[coder decodeObjectForKey:@"HFFont"] retain]; + _data = [[coder decodeObjectForKey:@"HFData"] retain]; + verticalOffset = (CGFloat)[coder decodeDoubleForKey:@"HFVerticalOffset"]; + horizontalContainerInset = (CGFloat)[coder decodeDoubleForKey:@"HFHorizontalContainerOffset"]; + defaultLineHeight = (CGFloat)[coder decodeDoubleForKey:@"HFDefaultLineHeight"]; + bytesBetweenVerticalGuides = (NSUInteger)[coder decodeInt64ForKey:@"HFBytesBetweenVerticalGuides"]; + startingLineBackgroundColorIndex = (NSUInteger)[coder decodeInt64ForKey:@"HFStartingLineBackgroundColorIndex"]; + rowBackgroundColors = [[coder decodeObjectForKey:@"HFRowBackgroundColors"] retain]; + _hftvflags.antialias = [coder decodeBoolForKey:@"HFAntialias"]; + _hftvflags.drawCallouts = [coder decodeBoolForKey:@"HFDrawCallouts"]; + _hftvflags.editable = [coder decodeBoolForKey:@"HFEditable"]; + return self; +} + +- (CGFloat)horizontalContainerInset { + return horizontalContainerInset; +} + +- (void)setHorizontalContainerInset:(CGFloat)inset { + horizontalContainerInset = inset; +} + +- (void)setBytesBetweenVerticalGuides:(NSUInteger)val { + bytesBetweenVerticalGuides = val; +} + +- (NSUInteger)bytesBetweenVerticalGuides { + return bytesBetweenVerticalGuides; +} + + +- (void)setFont:(NSFont *)val { + if (val != _font) { + [_font release]; + _font = [val retain]; + NSLayoutManager *manager = [[NSLayoutManager alloc] init]; + defaultLineHeight = [manager defaultLineHeightForFont:_font]; + [manager release]; + [self setNeedsDisplay:YES]; + } +} + +- (CGFloat)lineHeight { + return defaultLineHeight; +} + +/* The base implementation does not support font substitution, so we require that it be the base font. */ +- (NSFont *)fontAtSubstitutionIndex:(uint16_t)idx { + HFASSERT(idx == 0); + USE(idx); + return _font; +} + +- (NSRange)roundPartialByteRange:(NSRange)byteRange { + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + /* Get the left and right edges of the range */ + NSUInteger left = byteRange.location, right = NSMaxRange(byteRange); + + /* Round both to the left. This may make the range bigger or smaller, or empty! */ + left -= left % bytesPerCharacter; + right -= right % bytesPerCharacter; + + /* Done */ + HFASSERT(right >= left); + return NSMakeRange(left, right - left); + +} + +- (void)setNeedsDisplayForLinesInRange:(NSRange)lineRange { + // redisplay the lines in the given range + if (lineRange.length == 0) return; + NSUInteger firstLine = lineRange.location, lastLine = NSMaxRange(lineRange); + CGFloat lineHeight = [self lineHeight]; + CGFloat vertOffset = [self verticalOffset]; + CGFloat yOrigin = (firstLine - vertOffset) * lineHeight; + CGFloat lastLineBottom = (lastLine - vertOffset) * lineHeight; + NSRect bounds = [self bounds]; + NSRect dirtyRect = NSMakeRect(bounds.origin.x, bounds.origin.y + yOrigin, NSWidth(bounds), lastLineBottom - yOrigin); + [self setNeedsDisplayInRect:dirtyRect]; +} + +- (void)setData:(NSData *)val { + if (val != _data) { + NSUInteger oldLength = [_data length]; + NSUInteger newLength = [val length]; + const unsigned char *oldBytes = (const unsigned char *)[_data bytes]; + const unsigned char *newBytes = (const unsigned char *)[val bytes]; + NSUInteger firstDifferingIndex = HFIndexOfFirstByteThatDiffers(oldBytes, oldLength, newBytes, newLength); + if (firstDifferingIndex == NSUIntegerMax) { + /* Nothing to do! Data is identical! */ + } + else { + NSUInteger lastDifferingIndex = HFIndexOfLastByteThatDiffers(oldBytes, oldLength, newBytes, newLength); + HFASSERT(lastDifferingIndex != NSUIntegerMax); //if we have a first different byte, we must have a last different byte + /* Expand to encompass characters that they touch */ + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + firstDifferingIndex -= firstDifferingIndex % bytesPerCharacter; + lastDifferingIndex = HFRoundUpToMultipleInt(lastDifferingIndex, bytesPerCharacter); + + /* Now figure out the line range they touch */ + const NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger firstLine = firstDifferingIndex / bytesPerLine; + NSUInteger lastLine = HFDivideULRoundingUp(MAX(oldLength, newLength), bytesPerLine); + /* The +1 is for the following case - if we change the last character, then it may push the caret into the next line (even though there's no text there). This last line may have a background color, so we need to make it draw if it did not draw before (or vice versa - when deleting the last character which pulls the caret from the last line). */ + NSUInteger lastDifferingLine = (lastDifferingIndex == NSNotFound ? lastLine : HFDivideULRoundingUp(lastDifferingIndex + 1, bytesPerLine)); + if (lastDifferingLine > firstLine) { + [self setNeedsDisplayForLinesInRange:NSMakeRange(firstLine, lastDifferingLine - firstLine)]; + } + } + [_data release]; + _data = [val copy]; + [self _updateCaretTimer]; + } +} + +- (void)setStyles:(NSArray *)newStyles { + if (! [_styles isEqual:newStyles]) { + + /* Figure out which styles changed - that is, we want to compute those objects that are not in oldStyles or newStyles, but not both. */ + NSMutableSet *changedStyles = _styles ? [[NSMutableSet alloc] initWithArray:_styles] : [[NSMutableSet alloc] init]; + FOREACH(HFTextVisualStyleRun *, run, newStyles) { + if ([changedStyles containsObject:run]) { + [changedStyles removeObject:run]; + } + else { + [changedStyles addObject:run]; + } + } + + /* Now figure out the first and last indexes of changed ranges. */ + NSUInteger firstChangedIndex = NSUIntegerMax, lastChangedIndex = 0; + FOREACH(HFTextVisualStyleRun *, changedRun, changedStyles) { + NSRange range = [changedRun range]; + if (range.length > 0) { + firstChangedIndex = MIN(firstChangedIndex, range.location); + lastChangedIndex = MAX(lastChangedIndex, NSMaxRange(range) - 1); + } + } + + /* Don't need this any more */ + [changedStyles release]; + + /* Expand to cover all touched characters */ + NSUInteger bytesPerCharacter = [self bytesPerCharacter]; + firstChangedIndex -= firstChangedIndex % bytesPerCharacter; + lastChangedIndex = HFRoundUpToMultipleInt(lastChangedIndex, bytesPerCharacter); + + /* Figure out the changed lines, and trigger redisplay */ + if (firstChangedIndex <= lastChangedIndex) { + const NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger firstLine = firstChangedIndex / bytesPerLine; + NSUInteger lastLine = HFDivideULRoundingUp(lastChangedIndex, bytesPerLine); + [self setNeedsDisplayForLinesInRange:NSMakeRange(firstLine, lastLine - firstLine + 1)]; + } + + /* Do the usual Cocoa thing */ + [_styles release]; + _styles = [newStyles copy]; + } +} + +- (void)setVerticalOffset:(CGFloat)val { + if (val != verticalOffset) { + verticalOffset = val; + [self setNeedsDisplay:YES]; + } +} + +- (CGFloat)verticalOffset { + return verticalOffset; +} + +- (NSUInteger)startingLineBackgroundColorIndex { + return startingLineBackgroundColorIndex; +} + +- (void)setStartingLineBackgroundColorIndex:(NSUInteger)val { + startingLineBackgroundColorIndex = val; +} + +- (BOOL)isFlipped { + return YES; +} + +- (HFTextRepresenter *)representer { + return representer; +} + +- (void)dealloc { + HFUnregisterViewForWindowAppearanceChanges(self, _hftvflags.registeredForAppNotifications /* appToo */); + [caretTimer invalidate]; + [caretTimer release]; + [_font release]; + [_data release]; + [_styles release]; + [cachedSelectedRanges release]; + [callouts release]; + if(byteColoring) Block_release(byteColoring); + [super dealloc]; +} + +- (NSColor *)backgroundColorForEmptySpace { + NSArray *colors = [[self representer] rowBackgroundColors]; + if (! [colors count]) return [NSColor clearColor]; + else return colors[0]; +} + +- (NSColor *)backgroundColorForLine:(NSUInteger)line { + NSArray *colors = [[self representer] rowBackgroundColors]; + NSUInteger colorCount = [colors count]; + if (colorCount == 0) return [NSColor clearColor]; + NSUInteger colorIndex = (line + startingLineBackgroundColorIndex) % colorCount; + if (colorIndex == 0) return nil; //will be drawn by empty space + else return colors[colorIndex]; +} + +- (NSUInteger)bytesPerLine { + HFASSERT([self representer] != nil); + return [[self representer] bytesPerLine]; +} + +- (NSUInteger)bytesPerColumn { + HFASSERT([self representer] != nil); + return [[self representer] bytesPerColumn]; +} + +- (void)_drawDefaultLineBackgrounds:(NSRect)clip withLineHeight:(CGFloat)lineHeight maxLines:(NSUInteger)maxLines { + NSRect bounds = [self bounds]; + NSUInteger lineIndex; + NSRect lineRect = NSMakeRect(NSMinX(bounds), NSMinY(bounds), NSWidth(bounds), lineHeight); + if ([self showsFocusRing]) lineRect = NSInsetRect(lineRect, 2, 0); + lineRect.origin.y -= [self verticalOffset] * [self lineHeight]; + NSUInteger drawableLineIndex = 0; + NEW_ARRAY(NSRect, lineRects, maxLines); + NEW_ARRAY(NSColor*, lineColors, maxLines); + for (lineIndex = 0; lineIndex < maxLines; lineIndex++) { + NSRect clippedLineRect = NSIntersectionRect(lineRect, clip); + if (! NSIsEmptyRect(clippedLineRect)) { + NSColor *lineColor = [self backgroundColorForLine:lineIndex]; + if (lineColor) { + lineColors[drawableLineIndex] = lineColor; + lineRects[drawableLineIndex] = clippedLineRect; + drawableLineIndex++; + } + } + lineRect.origin.y += lineHeight; + } + + if (drawableLineIndex > 0) { + NSRectFillListWithColorsUsingOperation(lineRects, lineColors, drawableLineIndex, NSCompositeSourceOver); + } + + FREE_ARRAY(lineRects); + FREE_ARRAY(lineColors); +} + +- (HFTextVisualStyleRun *)styleRunForByteAtIndex:(NSUInteger)byteIndex { + HFTextVisualStyleRun *run = [[HFTextVisualStyleRun alloc] init]; + [run setRange:NSMakeRange(0, NSUIntegerMax)]; + [run setForegroundColor:[NSColor textColor]]; + return [run autorelease]; +} + +/* Given a list of rects and a parallel list of values, find cases of equal adjacent values, and union together their corresponding rects, deleting the second element from the list. Next, delete all nil values. Returns the new count of the list. */ +static size_t unionAndCleanLists(NSRect *rectList, id *valueList, size_t count) { + size_t trailing = 0, leading = 0; + while (leading < count) { + /* Copy our value left */ + valueList[trailing] = valueList[leading]; + rectList[trailing] = rectList[leading]; + + /* Skip one - no point unioning with ourselves */ + leading += 1; + + /* Sweep right, unioning until we reach a different value or the end */ + id targetValue = valueList[trailing]; + for (; leading < count; leading++) { + id testValue = valueList[leading]; + if (targetValue == testValue || (testValue && [targetValue isEqual:testValue])) { + /* Values match, so union the two rects */ + rectList[trailing] = NSUnionRect(rectList[trailing], rectList[leading]); + } + else { + /* Values don't match, we're done sweeping */ + break; + } + } + + /* We're done with this index */ + trailing += 1; + } + + /* trailing keeps track of how many values we have */ + count = trailing; + + /* Now do the same thing, except delete nil values */ + for (trailing = leading = 0; leading < count; leading++) { + if (valueList[leading] != nil) { + valueList[trailing] = valueList[leading]; + rectList[trailing] = rectList[leading]; + trailing += 1; + } + } + count = trailing; + + /* All done */ + return count; +} + +/* Draw vertical guidelines every four bytes */ +- (void)drawVerticalGuideLines:(NSRect)clip { + if (bytesBetweenVerticalGuides == 0) return; + + NSUInteger bytesPerLine = [self bytesPerLine]; + NSRect bounds = [self bounds]; + CGFloat advancePerCharacter = [self advancePerCharacter]; + CGFloat spaceAdvancement = advancePerCharacter / 2; + CGFloat advanceAmount = (advancePerCharacter + spaceAdvancement) * bytesBetweenVerticalGuides; + CGFloat lineOffset = (CGFloat)(NSMinX(bounds) + [self horizontalContainerInset] + advanceAmount - spaceAdvancement / 2.); + CGFloat endOffset = NSMaxX(bounds) - [self horizontalContainerInset]; + + NSUInteger numGuides = (bytesPerLine - 1) / bytesBetweenVerticalGuides; // -1 is a trick to avoid drawing the last line + NSUInteger guideIndex = 0, rectIndex = 0; + NEW_ARRAY(NSRect, lineRects, numGuides); + + while (lineOffset < endOffset && guideIndex < numGuides) { + NSRect lineRect = NSMakeRect(lineOffset - 1, NSMinY(bounds), 1, NSHeight(bounds)); + NSRect clippedLineRect = NSIntersectionRect(lineRect, clip); + if (! NSIsEmptyRect(clippedLineRect)) { + lineRects[rectIndex++] = clippedLineRect; + } + lineOffset += advanceAmount; + guideIndex++; + } + if (rectIndex > 0) { + [[NSColor gridColor] set]; + NSRectFillListUsingOperation(lineRects, rectIndex, NSCompositeSourceOver); + } + FREE_ARRAY(lineRects); +} + +- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount { + USE(byteCount); + UNIMPLEMENTED(); +} + +- (void)setByteColoring:(void (^)(uint8_t, uint8_t*, uint8_t*, uint8_t*, uint8_t*))coloring { + Block_release(byteColoring); + byteColoring = coloring ? Block_copy(coloring) : NULL; + [self setNeedsDisplay:YES]; +} + +- (void)drawByteColoringBackground:(NSRange)range inRect:(NSRect)rect { + if(!byteColoring) return; + + size_t width = (size_t)rect.size.width; + + // A rgba, 8-bit, single row image. + // +1 in case messing around with floats makes us overshoot a bit. + uint32_t *buffer = calloc(width+1, 4); + + const uint8_t *bytes = [_data bytes]; + bytes += range.location; + + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + CGFloat advancePerCharacter = [self advancePerCharacter]; + CGFloat advanceBetweenColumns = [self advanceBetweenColumns]; + + // For each character, draw the corresponding part of the image + CGFloat offset = [self horizontalContainerInset]; + for(NSUInteger i = 0; i < range.length; i++) { + uint8_t r, g, b, a; + byteColoring(bytes[i], &r, &g, &b, &a); + uint32_t c = ((uint32_t)r<<0) | ((uint32_t)g<<8) | ((uint32_t)b<<16) | ((uint32_t)a<<24); + memset_pattern4(&buffer[(size_t)offset], &c, 4*(size_t)(advancePerCharacter+1)); + offset += advancePerCharacter; + if(bytesPerColumn && (i+1) % bytesPerColumn == 0) + offset += advanceBetweenColumns; + } + + // Do a CGImage dance to draw the buffer + CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, 4 * width, NULL); + CGColorSpaceRef cgcolorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); + CGImageRef image = CGImageCreate(width, 1, 8, 32, 4 * width, cgcolorspace, + (CGBitmapInfo)kCGImageAlphaLast, provider, NULL, false, kCGRenderingIntentDefault); + CGContextDrawImage([[NSGraphicsContext currentContext] graphicsPort], NSRectToCGRect(rect), image); + CGColorSpaceRelease(cgcolorspace); + CGImageRelease(image); + CGDataProviderRelease(provider); + free(buffer); +} + +- (void)drawStyledBackgroundsForByteRange:(NSRange)range inRect:(NSRect)rect { + NSRect remainingRunRect = rect; + NSRange remainingRange = range; + + /* Our caller lies to us a little */ + remainingRunRect.origin.x += [self horizontalContainerInset]; + + const NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + + /* Here are the properties we care about */ + struct PropertyInfo_t { + SEL stylePropertyAccessor; // the selector we use to get the property + NSRect *rectList; // the list of rects corresponding to the property values + id *propertyValueList; // the list of the property values + size_t count; //list count, only gets set after cleaning up our lists + } propertyInfos[] = { + {.stylePropertyAccessor = @selector(backgroundColor)}, + {.stylePropertyAccessor = @selector(bookmarkStarts)}, + {.stylePropertyAccessor = @selector(bookmarkExtents)}, + {.stylePropertyAccessor = @selector(bookmarkEnds)} + }; + + /* Each list has the same capacity, and (initially) the same count */ + size_t listCount = 0, listCapacity = 0; + + /* The function pointer we use to get our property values */ + id (* const funcPtr)(id, SEL) = (id (*)(id, SEL))objc_msgSend; + + size_t propertyIndex; + const size_t propertyInfoCount = sizeof propertyInfos / sizeof *propertyInfos; + + while (remainingRange.length > 0) { + /* Get the next run for the remaining range. */ + HFTextVisualStyleRun *styleRun = [self styleRunForByteAtIndex:remainingRange.location]; + + /* The length of the run is the end of the style run or the end of the range we're given (whichever is smaller), minus the beginning of the range we care about. */ + NSUInteger runStart = remainingRange.location; + NSUInteger runLength = MIN(NSMaxRange(range), NSMaxRange([styleRun range])) - runStart; + + /* Get the width of this run and use it to compute the rect */ + CGFloat runRectWidth = [self totalAdvanceForBytesInRange:NSMakeRange(remainingRange.location, runLength)]; + NSRect runRect = remainingRunRect; + runRect.size.width = runRectWidth; + + /* Update runRect and remainingRunRect based on what we just learned */ + remainingRunRect.origin.x += runRectWidth; + remainingRunRect.size.width -= runRectWidth; + + /* Do a hack - if we end at a column boundary, subtract the advance between columns. If the next run has the same value for this property, then we'll end up unioning the rects together and the column gap will be filled. This is the primary purpose of this function. */ + if (bytesPerColumn > 0 && (runStart + runLength) % bytesPerColumn == 0) { + runRect.size.width -= MIN([self advanceBetweenColumns], runRect.size.width); + } + + /* Extend our lists if necessary */ + if (listCount == listCapacity) { + /* Our list is too small, extend it */ + listCapacity = listCapacity + 16; + + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + struct PropertyInfo_t *p = propertyInfos + propertyIndex; + p->rectList = check_realloc(p->rectList, listCapacity * sizeof *p->rectList); + p->propertyValueList = check_realloc(p->propertyValueList, listCapacity * sizeof *p->propertyValueList); + } + } + + /* Now append our values to our lists, even if it's nil */ + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + struct PropertyInfo_t *p = propertyInfos + propertyIndex; + id value = funcPtr(styleRun, p->stylePropertyAccessor); + p->rectList[listCount] = runRect; + p->propertyValueList[listCount] = value; + } + + listCount++; + + /* Update remainingRange */ + remainingRange.location += runLength; + remainingRange.length -= runLength; + + } + + /* Now clean up our lists, to delete the gaps we may have introduced */ + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + struct PropertyInfo_t *p = propertyInfos + propertyIndex; + p->count = unionAndCleanLists(p->rectList, p->propertyValueList, listCount); + } + + /* Finally we can draw them! First, draw byte backgrounds. */ + [self drawByteColoringBackground:range inRect:rect]; + + const struct PropertyInfo_t *p; + + /* Draw backgrounds */ + p = propertyInfos + 0; + if (p->count > 0) NSRectFillListWithColorsUsingOperation(p->rectList, p->propertyValueList, p->count, NSCompositeSourceOver); + + /* Clean up */ + for (propertyIndex = 0; propertyIndex < propertyInfoCount; propertyIndex++) { + p = propertyInfos + propertyIndex; + free(p->rectList); + free(p->propertyValueList); + } +} + +- (void)drawGlyphs:(const struct HFGlyph_t *)glyphs atPoint:(NSPoint)point withAdvances:(const CGSize *)advances withStyleRun:(HFTextVisualStyleRun *)styleRun count:(NSUInteger)glyphCount { + HFASSERT(glyphs != NULL); + HFASSERT(advances != NULL); + HFASSERT(glyphCount > 0); + if ([styleRun shouldDraw]) { + [styleRun set]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + + /* Get all the CGGlyphs together */ + NEW_ARRAY(CGGlyph, cgglyphs, glyphCount); + for (NSUInteger j=0; j < glyphCount; j++) { + cgglyphs[j] = glyphs[j].glyph; + } + + NSUInteger runStart = 0; + HFGlyphFontIndex runFontIndex = glyphs[0].fontIndex; + CGFloat runAdvance = 0; + for (NSUInteger i=1; i <= glyphCount; i++) { + /* Check if this run is finished, or if we are using a substitution font */ + if (i == glyphCount || glyphs[i].fontIndex != runFontIndex || runFontIndex > 0) { + /* Draw this run */ + NSFont *fontToUse = [self fontAtSubstitutionIndex:runFontIndex]; + [[fontToUse screenFont] set]; + CGContextSetTextPosition(ctx, point.x + runAdvance, point.y); + + if (runFontIndex > 0) { + /* A substitution font. Here we should only have one glyph */ + HFASSERT(i - runStart == 1); + /* Get the advance for this glyph. */ + NSSize nativeAdvance; + NSGlyph nativeGlyph = cgglyphs[runStart]; + [fontToUse getAdvancements:&nativeAdvance forGlyphs:&nativeGlyph count:1]; + if (nativeAdvance.width > advances[runStart].width) { + /* This glyph is too wide! We'll have to scale it. Here we only scale horizontally. */ + CGFloat horizontalScale = advances[runStart].width / nativeAdvance.width; + CGAffineTransform textCTM = CGContextGetTextMatrix(ctx); + textCTM.a *= horizontalScale; + CGContextSetTextMatrix(ctx, textCTM); + /* Note that we don't have to restore the text matrix, because the next call to set the font will overwrite it. */ + } + } + + /* Draw the glyphs */ + CGContextShowGlyphsWithAdvances(ctx, cgglyphs + runStart, advances + runStart, i - runStart); + + /* Record the new run */ + if (i < glyphCount) { + /* Sum the advances */ + for (NSUInteger j = runStart; j < i; j++) { + runAdvance += advances[j].width; + } + + /* Record the new run start and index */ + runStart = i; + runFontIndex = glyphs[i].fontIndex; + HFASSERT(runFontIndex != kHFGlyphFontIndexInvalid); + } + } + } + } +} + + +- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount { + USE(bytes); + USE(numBytes); + USE(offsetIntoLine); + USE(glyphs); + USE(advances); + USE(resultGlyphCount); + UNIMPLEMENTED_VOID(); +} + +- (void)extractGlyphsForBytes:(const unsigned char *)bytePtr range:(NSRange)byteRange intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances withInclusionRanges:(NSArray *)restrictingToRanges initialTextOffset:(CGFloat *)initialTextOffset resultingGlyphCount:(NSUInteger *)resultingGlyphCount { + NSParameterAssert(glyphs != NULL && advances != NULL && restrictingToRanges != nil && bytePtr != NULL); + NSRange priorIntersectionRange = {NSUIntegerMax, NSUIntegerMax}; + NSUInteger glyphBufferIndex = 0; + NSUInteger bytesPerLine = [self bytesPerLine]; + NSUInteger restrictionRangeCount = [restrictingToRanges count]; + for (NSUInteger rangeIndex = 0; rangeIndex < restrictionRangeCount; rangeIndex++) { + NSRange inclusionRange = [restrictingToRanges[rangeIndex] rangeValue]; + NSRange intersectionRange = NSIntersectionRange(inclusionRange, byteRange); + if (intersectionRange.length == 0) continue; + + NSUInteger offsetIntoLine = intersectionRange.location % bytesPerLine; + + NSRange byteRangeToSkip; + if (priorIntersectionRange.location == NSUIntegerMax) { + byteRangeToSkip = NSMakeRange(byteRange.location, intersectionRange.location - byteRange.location); + } + else { + HFASSERT(intersectionRange.location >= NSMaxRange(priorIntersectionRange)); + byteRangeToSkip.location = NSMaxRange(priorIntersectionRange); + byteRangeToSkip.length = intersectionRange.location - byteRangeToSkip.location; + } + + if (byteRangeToSkip.length > 0) { + CGFloat additionalAdvance = [self totalAdvanceForBytesInRange:byteRangeToSkip]; + if (glyphBufferIndex == 0) { + *initialTextOffset = *initialTextOffset + additionalAdvance; + } + else { + advances[glyphBufferIndex - 1].width += additionalAdvance; + } + } + + NSUInteger glyphCountForRange = NSUIntegerMax; + [self extractGlyphsForBytes:bytePtr + intersectionRange.location count:intersectionRange.length offsetIntoLine:offsetIntoLine intoArray:glyphs + glyphBufferIndex advances:advances + glyphBufferIndex resultingGlyphCount:&glyphCountForRange]; + HFASSERT(glyphCountForRange != NSUIntegerMax); + glyphBufferIndex += glyphCountForRange; + priorIntersectionRange = intersectionRange; + } + if (resultingGlyphCount) *resultingGlyphCount = glyphBufferIndex; +} + +- (void)drawTextWithClip:(NSRect)clip restrictingToTextInRanges:(NSArray *)restrictingToRanges { + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + NSRect bounds = [self bounds]; + CGFloat lineHeight = [self lineHeight]; + + CGAffineTransform textTransform = CGContextGetTextMatrix(ctx); + CGContextSetTextDrawingMode(ctx, kCGTextFill); + + NSUInteger lineStartIndex, bytesPerLine = [self bytesPerLine]; + NSData *dataObject = [self data]; + NSFont *fontObject = [[self font] screenFont]; + //const NSUInteger bytesPerChar = [self bytesPerCharacter]; + const NSUInteger byteCount = [dataObject length]; + + const unsigned char * const bytePtr = [dataObject bytes]; + + NSRect lineRectInBoundsSpace = NSMakeRect(NSMinX(bounds), NSMinY(bounds), NSWidth(bounds), lineHeight); + lineRectInBoundsSpace.origin.y -= [self verticalOffset] * lineHeight; + + /* Start us off with the horizontal inset and move the baseline down by the ascender so our glyphs just graze the top of our view */ + textTransform.tx += [self horizontalContainerInset]; + textTransform.ty += [fontObject ascender] - lineHeight * [self verticalOffset]; + NSUInteger lineIndex = 0; + const NSUInteger maxGlyphCount = [self maximumGlyphCountForByteCount:bytesPerLine]; + NEW_ARRAY(struct HFGlyph_t, glyphs, maxGlyphCount); + NEW_ARRAY(CGSize, advances, maxGlyphCount); + for (lineStartIndex = 0; lineStartIndex < byteCount; lineStartIndex += bytesPerLine) { + if (lineStartIndex > 0) { + textTransform.ty += lineHeight; + lineRectInBoundsSpace.origin.y += lineHeight; + } + if (NSIntersectsRect(lineRectInBoundsSpace, clip)) { + const NSUInteger bytesInThisLine = MIN(bytesPerLine, byteCount - lineStartIndex); + + /* Draw the backgrounds of any styles. */ + [self drawStyledBackgroundsForByteRange:NSMakeRange(lineStartIndex, bytesInThisLine) inRect:lineRectInBoundsSpace]; + + NSUInteger byteIndexInLine = 0; + CGFloat advanceIntoLine = 0; + while (byteIndexInLine < bytesInThisLine) { + const NSUInteger byteIndex = lineStartIndex + byteIndexInLine; + HFTextVisualStyleRun *styleRun = [self styleRunForByteAtIndex:byteIndex]; + HFASSERT(styleRun != nil); + HFASSERT(byteIndex >= [styleRun range].location); + const NSUInteger bytesInThisRun = MIN(NSMaxRange([styleRun range]) - byteIndex, bytesInThisLine - byteIndexInLine); + const NSRange characterRange = [self roundPartialByteRange:NSMakeRange(byteIndex, bytesInThisRun)]; + if (characterRange.length > 0) { + NSUInteger resultGlyphCount = 0; + CGFloat initialTextOffset = 0; + if (restrictingToRanges == nil) { + [self extractGlyphsForBytes:bytePtr + characterRange.location count:characterRange.length offsetIntoLine:byteIndexInLine intoArray:glyphs advances:advances resultingGlyphCount:&resultGlyphCount]; + } + else { + [self extractGlyphsForBytes:bytePtr range:NSMakeRange(byteIndex, bytesInThisRun) intoArray:glyphs advances:advances withInclusionRanges:restrictingToRanges initialTextOffset:&initialTextOffset resultingGlyphCount:&resultGlyphCount]; + } + HFASSERT(resultGlyphCount <= maxGlyphCount); + +#if ! NDEBUG + for (NSUInteger q=0; q < resultGlyphCount; q++) { + HFASSERT(glyphs[q].fontIndex != kHFGlyphFontIndexInvalid); + } +#endif + + if (resultGlyphCount > 0) { + textTransform.tx += initialTextOffset + advanceIntoLine; + CGContextSetTextMatrix(ctx, textTransform); + /* Draw them */ + [self drawGlyphs:glyphs atPoint:NSMakePoint(textTransform.tx, textTransform.ty) withAdvances:advances withStyleRun:styleRun count:resultGlyphCount]; + + /* Undo the work we did before so as not to screw up the next run */ + textTransform.tx -= initialTextOffset + advanceIntoLine; + + /* Record how far into our line this made us move */ + NSUInteger glyphIndex; + for (glyphIndex = 0; glyphIndex < resultGlyphCount; glyphIndex++) { + advanceIntoLine += advances[glyphIndex].width; + } + } + } + byteIndexInLine += bytesInThisRun; + } + } + else if (NSMinY(lineRectInBoundsSpace) > NSMaxY(clip)) { + break; + } + lineIndex++; + } + FREE_ARRAY(glyphs); + FREE_ARRAY(advances); +} + + +- (void)drawFocusRingWithClip:(NSRect)clip { + USE(clip); + [NSGraphicsContext saveGraphicsState]; + NSSetFocusRingStyle(NSFocusRingOnly); + [[NSColor clearColor] set]; + NSRectFill([self bounds]); + [NSGraphicsContext restoreGraphicsState]; +} + +- (BOOL)shouldDrawCallouts { + return _hftvflags.drawCallouts; +} + +- (void)setShouldDrawCallouts:(BOOL)val { + _hftvflags.drawCallouts = val; + [self setNeedsDisplay:YES]; +} + +- (void)drawBookmarksWithClip:(NSRect)clip { + if([self shouldDrawCallouts]) { + /* Figure out which callouts we're going to draw */ + NSRect allCalloutsRect = NSZeroRect; + NSMutableArray *localCallouts = [[NSMutableArray alloc] initWithCapacity:[callouts count]]; + FOREACH(HFRepresenterTextViewCallout *, callout, [callouts objectEnumerator]) { + NSRect calloutRect = [callout rect]; + if (NSIntersectsRect(clip, calloutRect)) { + [localCallouts addObject:callout]; + allCalloutsRect = NSUnionRect(allCalloutsRect, calloutRect); + } + } + allCalloutsRect = NSIntersectionRect(allCalloutsRect, clip); + + if ([localCallouts count]) { + /* Draw shadows first */ + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + CGContextBeginTransparencyLayerWithRect(ctx, NSRectToCGRect(allCalloutsRect), NULL); + FOREACH(HFRepresenterTextViewCallout *, callout, localCallouts) { + [callout drawShadowWithClip:clip]; + } + CGContextEndTransparencyLayer(ctx); + + FOREACH(HFRepresenterTextViewCallout *, newCallout, localCallouts) { + // NSRect rect = [callout rect]; + // [[NSColor greenColor] set]; + // NSFrameRect(rect); + [newCallout drawWithClip:clip]; + } + } + [localCallouts release]; + } +} + +- (void)drawRect:(NSRect)clip { + [[self backgroundColorForEmptySpace] set]; + NSRectFillUsingOperation(clip, NSCompositeSourceOver); + BOOL antialias = [self shouldAntialias]; + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + + [[self.font screenFont] set]; + + if ([self showsFocusRing]) { + NSWindow *window = [self window]; + if (self == [window firstResponder] && [window isKeyWindow]) { + [self drawFocusRingWithClip:clip]; + } + } + + NSUInteger bytesPerLine = [self bytesPerLine]; + if (bytesPerLine == 0) return; + NSUInteger byteCount = [_data length]; + + [self _drawDefaultLineBackgrounds:clip withLineHeight:[self lineHeight] maxLines:ll2l(HFRoundUpToNextMultipleSaturate(byteCount, bytesPerLine) / bytesPerLine)]; + [self drawSelectionIfNecessaryWithClip:clip]; + + NSColor *textColor = [NSColor textColor]; + [textColor set]; + + if (! antialias) { + CGContextSaveGState(ctx); + CGContextSetShouldAntialias(ctx, NO); + } + [self drawTextWithClip:clip restrictingToTextInRanges:nil]; + if (! antialias) { + CGContextRestoreGState(ctx); + } + + // Vertical dividers only make sense in single byte mode. + if ([self _effectiveBytesPerColumn] == 1) { + [self drawVerticalGuideLines:clip]; + } + + [self drawCaretIfNecessaryWithClip:clip]; + + [self drawBookmarksWithClip:clip]; +} + +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forRange:(NSRange)byteRange { + HFASSERT(edge == NSMinXEdge || edge == NSMaxXEdge || edge == NSMinYEdge || edge == NSMaxYEdge); + const NSUInteger bytesPerLine = [self bytesPerLine]; + CGFloat lineHeight = [self lineHeight]; + CGFloat vertOffset = [self verticalOffset]; + NSUInteger firstLine = byteRange.location / bytesPerLine, lastLine = (NSMaxRange(byteRange) - 1) / bytesPerLine; + NSRect result = NSZeroRect; + + if (edge == NSMinYEdge || edge == NSMaxYEdge) { + /* This is the top (MinY) or bottom (MaxY). We only have to look at one line. */ + NSUInteger lineIndex = (edge == NSMinYEdge ? firstLine : lastLine); + NSRange lineRange = NSMakeRange(lineIndex * bytesPerLine, bytesPerLine); + NSRange intersection = NSIntersectionRange(lineRange, byteRange); + HFASSERT(intersection.length > 0); + CGFloat yOrigin = (lineIndex - vertOffset) * lineHeight; + CGFloat xStart = [self originForCharacterAtByteIndex:intersection.location].x; + CGFloat xEnd = [self originForCharacterAtByteIndex:NSMaxRange(intersection) - 1].x + [self advancePerCharacter]; + result = NSMakeRect(xStart, yOrigin, xEnd - xStart, 0); + } + else { + if (firstLine == lastLine) { + /* We only need to consider this one line */ + NSRange lineRange = NSMakeRange(firstLine * bytesPerLine, bytesPerLine); + NSRange intersection = NSIntersectionRange(lineRange, byteRange); + HFASSERT(intersection.length > 0); + CGFloat yOrigin = (firstLine - vertOffset) * lineHeight; + CGFloat xCoord; + if (edge == NSMinXEdge) { + xCoord = [self originForCharacterAtByteIndex:intersection.location].x; + } + else { + xCoord = [self originForCharacterAtByteIndex:NSMaxRange(intersection) - 1].x + [self advancePerCharacter]; + } + result = NSMakeRect(xCoord, yOrigin, 0, lineHeight); + } + else { + /* We have more than one line. If we are asking for the left edge, sum up the left edge of every line but the first, and handle the first specially. Likewise for the right edge (except handle the last specially) */ + BOOL includeFirstLine, includeLastLine; + CGFloat xCoord; + if (edge == NSMinXEdge) { + /* Left edge, include the first line only if it starts at the beginning of the line or there's only one line */ + includeFirstLine = (byteRange.location % bytesPerLine == 0); + includeLastLine = YES; + xCoord = [self horizontalContainerInset]; + } + else { + /* Right edge, include the last line only if it starts at the beginning of the line or there's only one line */ + includeFirstLine = YES; + includeLastLine = (NSMaxRange(byteRange) % bytesPerLine == 0); + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + /* Don't add in space for the advance after the last column, hence subtract 1. */ + NSUInteger numColumns = (bytesPerColumn ? (bytesPerLine / bytesPerColumn - 1) : 0); + xCoord = [self horizontalContainerInset] + ([self advancePerCharacter] * bytesPerLine / [self bytesPerCharacter]) + [self advanceBetweenColumns] * numColumns; + } + NSUInteger firstLineToInclude = (includeFirstLine ? firstLine : firstLine + 1), lastLineToInclude = (includeLastLine ? lastLine : lastLine - 1); + result = NSMakeRect(xCoord, (firstLineToInclude - [self verticalOffset]) * lineHeight, 0, (lastLineToInclude - firstLineToInclude + 1) * lineHeight); + } + } + return result; +} + +- (NSUInteger)availableLineCount { + CGFloat result = (CGFloat)ceil(NSHeight([self bounds]) / [self lineHeight]); + HFASSERT(result >= 0.); + HFASSERT(result <= NSUIntegerMax); + return (NSUInteger)result; +} + +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight { + return viewHeight / [self lineHeight]; +} + +- (void)setFrameSize:(NSSize)size { + NSUInteger currentBytesPerLine = [self bytesPerLine]; + double currentLineCount = [self maximumAvailableLinesForViewHeight:NSHeight([self bounds])]; + [super setFrameSize:size]; + NSUInteger newBytesPerLine = [self maximumBytesPerLineForViewWidth:size.width]; + double newLineCount = [self maximumAvailableLinesForViewHeight:NSHeight([self bounds])]; + HFControllerPropertyBits bits = 0; + if (newBytesPerLine != currentBytesPerLine) bits |= (HFControllerBytesPerLine | HFControllerDisplayedLineRange); + if (newLineCount != currentLineCount) bits |= HFControllerDisplayedLineRange; + if (bits) [[self representer] representerChangedProperties:bits]; +} + +- (CGFloat)advanceBetweenColumns { + UNIMPLEMENTED(); +} + +- (CGFloat)advancePerCharacter { + UNIMPLEMENTED(); +} + +- (CGFloat)advancePerColumn { + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + if (bytesPerColumn == 0) { + return 0; + } + else { + return [self advancePerCharacter] * (bytesPerColumn / [self bytesPerCharacter]) + [self advanceBetweenColumns]; + } +} + +- (CGFloat)totalAdvanceForBytesInRange:(NSRange)range { + if (range.length == 0) return 0; + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + HFASSERT(bytesPerColumn == 0 || [self bytesPerLine] % bytesPerColumn == 0); + CGFloat result = (range.length * [self advancePerCharacter] / [self bytesPerCharacter]) ; + if (bytesPerColumn > 0) { + NSUInteger numColumnSpaces = NSMaxRange(range) / bytesPerColumn - range.location / bytesPerColumn; //note that integer division does not distribute + result += numColumnSpaces * [self advanceBetweenColumns]; + } + return result; +} + +/* Returns the number of bytes in a character, e.g. if we are UTF-16 this would be 2. */ +- (NSUInteger)bytesPerCharacter { + return 1; +} + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth { + CGFloat availableSpace = (CGFloat)(viewWidth - 2. * [self horizontalContainerInset]); + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn], bytesPerCharacter = [self bytesPerCharacter]; + if (bytesPerColumn == 0) { + /* No columns */ + NSUInteger numChars = (NSUInteger)(availableSpace / [self advancePerCharacter]); + /* Return it, except it's at least one character */ + return MAX(numChars, 1u) * bytesPerCharacter; + } + else { + /* We have some columns */ + CGFloat advancePerColumn = [self advancePerColumn]; + //spaceRequiredForNColumns = N * (advancePerColumn) - spaceBetweenColumns + CGFloat fractionalColumns = (availableSpace + [self advanceBetweenColumns]) / advancePerColumn; + NSUInteger columnCount = (NSUInteger)fmax(1., HFFloor(fractionalColumns)); + return columnCount * bytesPerColumn; + } +} + + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + HFASSERT(bytesPerLine > 0); + NSUInteger bytesPerColumn = [self _effectiveBytesPerColumn]; + CGFloat result; + if (bytesPerColumn == 0) { + result = (CGFloat)((2. * [self horizontalContainerInset]) + [self advancePerCharacter] * (bytesPerLine / [self bytesPerCharacter])); + } + else { + HFASSERT(bytesPerLine % bytesPerColumn == 0); + result = (CGFloat)((2. * [self horizontalContainerInset]) + [self advancePerColumn] * (bytesPerLine / bytesPerColumn) - [self advanceBetweenColumns]); + } + return result; +} + +- (BOOL)isEditable { + return _hftvflags.editable; +} + +- (void)setEditable:(BOOL)val { + if (val != _hftvflags.editable) { + _hftvflags.editable = val; + [self _updateCaretTimer]; + } +} + +- (BOOL)shouldAntialias { + return _hftvflags.antialias; +} + +- (void)setShouldAntialias:(BOOL)val { + _hftvflags.antialias = !!val; + [self setNeedsDisplay:YES]; +} + +- (BOOL)behavesAsTextField { + return [[self representer] behavesAsTextField]; +} + +- (BOOL)showsFocusRing { + return [[self representer] behavesAsTextField]; +} + +- (BOOL)isWithinMouseDown { + return _hftvflags.withinMouseDown; +} + +- (void)_windowDidChangeKeyStatus:(NSNotification *)note { + USE(note); + [self _updateCaretTimer]; + if ([[note name] isEqualToString:NSWindowDidBecomeKeyNotification]) { + [self _forceCaretOnIfHasCaretTimer]; + } + if ([self showsFocusRing] && self == [[self window] firstResponder]) { + [[self superview] setNeedsDisplayInRect:NSInsetRect([self frame], -6, -6)]; + } + [self setNeedsDisplay:YES]; +} + +- (void)viewDidMoveToWindow { + [self _updateCaretTimer]; + HFRegisterViewForWindowAppearanceChanges(self, @selector(_windowDidChangeKeyStatus:), ! _hftvflags.registeredForAppNotifications); + _hftvflags.registeredForAppNotifications = YES; + [super viewDidMoveToWindow]; +} + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow { + HFUnregisterViewForWindowAppearanceChanges(self, NO /* appToo */); + [super viewWillMoveToWindow:newWindow]; +} + +/* Computes the character at the given index for selection, properly handling the case where the point is outside the bounds */ +- (NSUInteger)characterAtPointForSelection:(NSPoint)point { + NSPoint mungedPoint = point; + // shift us right by half an advance so that we trigger at the midpoint of each character, rather than at the x origin + mungedPoint.x += [self advancePerCharacter] / (CGFloat)2.; + // make sure we're inside the bounds + const NSRect bounds = [self bounds]; + mungedPoint.x = HFMax(NSMinX(bounds), mungedPoint.x); + mungedPoint.x = HFMin(NSMaxX(bounds), mungedPoint.x); + mungedPoint.y = HFMax(NSMinY(bounds), mungedPoint.y); + mungedPoint.y = HFMin(NSMaxY(bounds), mungedPoint.y); + return [self indexOfCharacterAtPoint:mungedPoint]; +} + +- (NSUInteger)maximumCharacterIndex { + //returns the maximum character index that the selection may lie on. It is one beyond the last byte index, to represent the cursor at the end of the document. + return [[self data] length] / [self bytesPerCharacter]; +} + +- (void)mouseDown:(NSEvent *)event { + HFASSERT(_hftvflags.withinMouseDown == 0); + _hftvflags.withinMouseDown = 1; + [self _forceCaretOnIfHasCaretTimer]; + NSPoint mouseDownLocation = [self convertPoint:[event locationInWindow] fromView:nil]; + NSUInteger characterIndex = [self characterAtPointForSelection:mouseDownLocation]; + + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); //characterIndex may be one beyond the last index, to represent the cursor at the end of the document + [[self representer] beginSelectionWithEvent:event forCharacterIndex:characterIndex]; + + /* Drive the event loop in event tracking mode until we're done */ + HFASSERT(_hftvflags.receivedMouseUp == NO); //paranoia - detect any weird recursive invocations + NSDate *endDate = [NSDate distantFuture]; + + /* Start periodic events for autoscroll */ + [NSEvent startPeriodicEventsAfterDelay:0.1 withPeriod:0.05]; + + NSPoint autoscrollLocation = mouseDownLocation; + while (! _hftvflags.receivedMouseUp) { + @autoreleasepool { + NSEvent *ev = [NSApp nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSPeriodicMask untilDate:endDate inMode:NSEventTrackingRunLoopMode dequeue:YES]; + + if ([ev type] == NSPeriodic) { + // autoscroll if drag is out of view bounds + CGFloat amountToScroll = 0; + NSRect bounds = [self bounds]; + if (autoscrollLocation.y < NSMinY(bounds)) { + amountToScroll = (autoscrollLocation.y - NSMinY(bounds)) / [self lineHeight]; + } + else if (autoscrollLocation.y > NSMaxY(bounds)) { + amountToScroll = (autoscrollLocation.y - NSMaxY(bounds)) / [self lineHeight]; + } + if (amountToScroll != 0.) { + [[[self representer] controller] scrollByLines:amountToScroll]; + characterIndex = [self characterAtPointForSelection:autoscrollLocation]; + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); + [[self representer] continueSelectionWithEvent:ev forCharacterIndex:characterIndex]; + } + } + else if ([ev type] == NSLeftMouseDragged) { + autoscrollLocation = [self convertPoint:[ev locationInWindow] fromView:nil]; + } + + [NSApp sendEvent:ev]; + } // @autoreleasepool + } + + [NSEvent stopPeriodicEvents]; + + _hftvflags.receivedMouseUp = NO; + _hftvflags.withinMouseDown = 0; +} + +- (void)mouseDragged:(NSEvent *)event { + if (! _hftvflags.withinMouseDown) return; + NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; + NSUInteger characterIndex = [self characterAtPointForSelection:location]; + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); + [[self representer] continueSelectionWithEvent:event forCharacterIndex:characterIndex]; +} + +- (void)mouseUp:(NSEvent *)event { + if (! _hftvflags.withinMouseDown) return; + NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; + NSUInteger characterIndex = [self characterAtPointForSelection:location]; + characterIndex = MIN(characterIndex, [self maximumCharacterIndex]); + [[self representer] endSelectionWithEvent:event forCharacterIndex:characterIndex]; + _hftvflags.receivedMouseUp = YES; +} + +- (void)keyDown:(NSEvent *)event { + HFASSERT(event != NULL); + [self interpretKeyEvents:@[event]]; +} + +- (void)scrollWheel:(NSEvent *)event { + [[self representer] scrollWheel:event]; +} + +- (void)insertText:(id)string { + if (! [self isEditable]) { + NSBeep(); + } + else { + if ([string isKindOfClass:[NSAttributedString class]]) string = [string string]; + [NSCursor setHiddenUntilMouseMoves:YES]; + [[self representer] insertText:string]; + } +} + +- (BOOL)handleCommand:(SEL)sel { + if (sel == @selector(insertTabIgnoringFieldEditor:)) { + [self insertText:@"\t"]; + } + else if ([self respondsToSelector:sel]) { + [self performSelector:sel withObject:nil]; + } + else { + return NO; + } + return YES; +} + +- (void)doCommandBySelector:(SEL)sel { + HFRepresenter *rep = [self representer]; + // NSLog(@"%s%s", _cmd, sel); + if ([self handleCommand:sel]) { + /* Nothing to do */ + } + else if ([rep respondsToSelector:sel]) { + [rep performSelector:sel withObject:self]; + } + else { + [super doCommandBySelector:sel]; + } +} + +- (IBAction)selectAll:sender { + [[self representer] selectAll:sender]; +} + +/* Indicates whether at least one byte is selected */ +- (BOOL)_selectionIsNonEmpty { + NSArray *selection = [[[self representer] controller] selectedContentsRanges]; + FOREACH(HFRangeWrapper *, rangeWrapper, selection) { + if ([rangeWrapper HFRange].length > 0) return YES; + } + return NO; +} + +- (SEL)_pasteboardOwnerStringTypeWritingSelector { + UNIMPLEMENTED(); +} + +- (void)paste:sender { + if (! [self isEditable]) { + NSBeep(); + } + else { + USE(sender); + [[self representer] pasteBytesFromPasteboard:[NSPasteboard generalPasteboard]]; + } +} + +- (void)copy:sender { + USE(sender); + [[self representer] copySelectedBytesToPasteboard:[NSPasteboard generalPasteboard]]; +} + +- (void)cut:sender { + USE(sender); + [[self representer] cutSelectedBytesToPasteboard:[NSPasteboard generalPasteboard]]; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)item { + SEL action = [item action]; + if (action == @selector(selectAll:)) return YES; + else if (action == @selector(cut:)) return [[self representer] canCut]; + else if (action == @selector(copy:)) return [self _selectionIsNonEmpty]; + else if (action == @selector(paste:)) return [[self representer] canPasteFromPasteboard:[NSPasteboard generalPasteboard]]; + else return YES; +} + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterTextViewCallout.h b/bsnes/gb/HexFiend/HFRepresenterTextViewCallout.h new file mode 100644 index 00000000..42ae7e3a --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterTextViewCallout.h @@ -0,0 +1,31 @@ +// +// HFRepresenterTextViewCallout.h +// HexFiend_2 +// +// Copyright 2011 ridiculous_fish. All rights reserved. +// + +#import + +@class HFRepresenterTextView; + +#define kHFRepresenterTextViewCalloutMaxGlyphCount 2u + +@interface HFRepresenterTextViewCallout : NSObject { + CGFloat rotation; + NSPoint tipOrigin; + NSPoint pinStart, pinEnd; +} + +@property(nonatomic) NSInteger byteOffset; +@property(nonatomic, copy) NSColor *color; +@property(nonatomic, copy) NSString *label; +@property(nonatomic, retain) id representedObject; +@property(readonly) NSRect rect; + ++ (void)layoutCallouts:(NSArray *)callouts inView:(HFRepresenterTextView *)textView; + +- (void)drawShadowWithClip:(NSRect)clip; +- (void)drawWithClip:(NSRect)clip; + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterTextViewCallout.m b/bsnes/gb/HexFiend/HFRepresenterTextViewCallout.m new file mode 100644 index 00000000..bb4b58e8 --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterTextViewCallout.m @@ -0,0 +1,477 @@ +// +// HFRepresenterTextViewCallout.m +// HexFiend_2 +// +// Copyright 2011 ridiculous_fish. All rights reserved. +// + +#import "HFRepresenterTextViewCallout.h" +#import "HFRepresenterTextView.h" + +static const CGFloat HFTeardropRadius = 12; +static const CGFloat HFTeadropTipScale = 2.5; + +static const CGFloat HFShadowXOffset = -6; +static const CGFloat HFShadowYOffset = 0; +static const CGFloat HFShadowOffscreenHack = 3100; + +static NSPoint rotatePoint(NSPoint center, NSPoint point, CGFloat percent) { + CGFloat radians = percent * M_PI * 2; + CGFloat x = point.x - center.x; + CGFloat y = point.y - center.y; + CGFloat newX = x * cos(radians) + y * sin(radians); + CGFloat newY = x * -sin(radians) + y * cos(radians); + return NSMakePoint(center.x + newX, center.y + newY); +} + +static NSPoint scalePoint(NSPoint center, NSPoint point, CGFloat percent) { + CGFloat x = point.x - center.x; + CGFloat y = point.y - center.y; + CGFloat newX = x * percent; + CGFloat newY = y * percent; + return NSMakePoint(center.x + newX, center.y + newY); +} + +static NSBezierPath *copyTeardropPath(void) { + static NSBezierPath *sPath = nil; + if (! sPath) { + + CGFloat radius = HFTeardropRadius; + CGFloat rotation = 0; + CGFloat droppiness = .15; + CGFloat tipScale = HFTeadropTipScale; + CGFloat tipLengthFromCenter = radius * tipScale; + NSPoint bulbCenter = NSMakePoint(-tipLengthFromCenter, 0); + + NSPoint triangleCenter = rotatePoint(bulbCenter, NSMakePoint(bulbCenter.x + radius, bulbCenter.y), rotation); + NSPoint dropCorner1 = rotatePoint(bulbCenter, triangleCenter, droppiness / 2); + NSPoint dropCorner2 = rotatePoint(bulbCenter, triangleCenter, -droppiness / 2); + NSPoint dropTip = scalePoint(bulbCenter, triangleCenter, tipScale); + + NSBezierPath *path = [[NSBezierPath alloc] init]; + [path appendBezierPathWithArcWithCenter:bulbCenter radius:radius startAngle:-rotation * 360 + droppiness * 180. endAngle:-rotation * 360 - droppiness * 180. clockwise:NO]; + + [path moveToPoint:dropCorner1]; + [path lineToPoint:dropTip]; + [path lineToPoint:dropCorner2]; + [path closePath]; + + sPath = path; + } + return [sPath retain]; +} + + +@implementation HFRepresenterTextViewCallout + +/* A helpful struct for representing a wedge (portion of a circle). Wedges are counterclockwise. */ +typedef struct { + double offset; // 0 <= offset < 1 + double length; // 0 <= length <= 1 +} Wedge_t; + + +static inline double normalizeAngle(double x) { + /* Convert an angle to the range [0, 1). We typically only generate angles that are off by a full rotation, so a loop isn't too bad. */ + while (x >= 1.) x -= 1.; + while (x < 0.) x += 1.; + return x; +} + +static inline double distanceCCW(double a, double b) { return normalizeAngle(b-a); } + +static inline double wedgeMax(Wedge_t wedge) { + return normalizeAngle(wedge.offset + wedge.length); +} + +/* Computes the smallest wedge containing the two given wedges. Compute the wedge from the min of one to the furthest part of the other, and pick the smaller. */ +static Wedge_t wedgeUnion(Wedge_t wedge1, Wedge_t wedge2) { + // empty wedges don't participate + if (wedge1.length <= 0) return wedge2; + if (wedge2.length <= 0) return wedge1; + + Wedge_t union1 = wedge1; + union1.length = fmin(1., fmax(union1.length, distanceCCW(union1.offset, wedge2.offset) + wedge2.length)); + + Wedge_t union2 = wedge2; + union2.length = fmin(1., fmax(union2.length, distanceCCW(union2.offset, wedge1.offset) + wedge1.length)); + + Wedge_t result = (union1.length <= union2.length ? union1 : union2); + HFASSERT(result.length <= 1); + return result; +} + +- (instancetype)init { + self = [super init]; + if (self) { + // Initialization code here. + } + + return self; +} + +- (void)dealloc { + [_representedObject release]; + [_color release]; + [_label release]; + [super dealloc]; +} + +- (NSComparisonResult)compare:(HFRepresenterTextViewCallout *)callout { + return [_representedObject compare:callout.representedObject]; +} + +static Wedge_t computeForbiddenAngle(double distanceFromEdge, double angleToEdge) { + Wedge_t newForbiddenAngle; + + /* This is how far it is to the center of our teardrop */ + const double teardropLength = HFTeardropRadius * HFTeadropTipScale; + + if (distanceFromEdge <= 0) { + /* We're above or below. */ + if (-distanceFromEdge >= (teardropLength + HFTeardropRadius)) { + /* We're so far above or below we won't be visible at all. No hope. */ + newForbiddenAngle = (Wedge_t){.offset = 0, .length = 1}; + } else { + /* We're either above or below the bounds, but there's a hope we can be visible */ + + double invertedAngleToEdge = normalizeAngle(angleToEdge + .5); + double requiredAngle; + if (-distanceFromEdge >= teardropLength) { + // We're too far north or south that all we can do is point in the right direction + requiredAngle = 0; + } else { + // By confining ourselves to required angles, we can make ourselves visible + requiredAngle = acos(-distanceFromEdge / teardropLength) / (2 * M_PI); + } + // Require at least a small spread + requiredAngle = fmax(requiredAngle, .04); + + double requiredMin = invertedAngleToEdge - requiredAngle; + double requiredMax = invertedAngleToEdge + requiredAngle; + + newForbiddenAngle = (Wedge_t){.offset = requiredMax, .length = distanceCCW(requiredMax, requiredMin) }; + } + } else if (distanceFromEdge < teardropLength) { + // We're onscreen, but some angle will be forbidden + double forbiddenAngle = acos(distanceFromEdge / teardropLength) / (2 * M_PI); + + // This is a wedge out of the top (or bottom) + newForbiddenAngle = (Wedge_t){.offset = angleToEdge - forbiddenAngle, .length = 2 * forbiddenAngle}; + } else { + /* Nothing prohibited at all */ + newForbiddenAngle = (Wedge_t){0, 0}; + } + return newForbiddenAngle; +} + + +static double distanceMod1(double a, double b) { + /* Assuming 0 <= a, b < 1, returns the distance between a and b, mod 1 */ + if (a > b) { + return fmin(a-b, b-a+1); + } else { + return fmin(b-a, a-b+1); + } +} + ++ (void)layoutCallouts:(NSArray *)callouts inView:(HFRepresenterTextView *)textView { + + // Keep track of how many drops are at a given location + NSCountedSet *dropsPerByteLoc = [[NSCountedSet alloc] init]; + + const CGFloat lineHeight = [textView lineHeight]; + const NSRect bounds = [textView bounds]; + + NSMutableArray *remainingCallouts = [[callouts mutableCopy] autorelease]; + [remainingCallouts sortUsingSelector:@selector(compare:)]; + + while ([remainingCallouts count] > 0) { + /* Get the next callout to lay out */ + const NSInteger byteLoc = [remainingCallouts[0] byteOffset]; + + /* Get all the callouts that share that byteLoc */ + NSMutableArray *sharedCallouts = [NSMutableArray array]; + FOREACH(HFRepresenterTextViewCallout *, testCallout, remainingCallouts) { + if ([testCallout byteOffset] == byteLoc) { + [sharedCallouts addObject:testCallout]; + } + } + + /* We expect to get at least one */ + const NSUInteger calloutCount = [sharedCallouts count]; + HFASSERT(calloutCount > 0); + + /* Get the character origin */ + const NSPoint characterOrigin = [textView originForCharacterAtByteIndex:byteLoc]; + + Wedge_t forbiddenAngle = {0, 0}; + + // Compute how far we are from the top (or bottom) + BOOL isNearerTop = (characterOrigin.y < NSMidY(bounds)); + double verticalDistance = (isNearerTop ? characterOrigin.y - NSMinY(bounds) : NSMaxY(bounds) - characterOrigin.y); + forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(verticalDistance, (isNearerTop ? .25 : .75))); + + // Compute how far we are from the left (or right) + BOOL isNearerLeft = (characterOrigin.x < NSMidX(bounds)); + double horizontalDistance = (isNearerLeft ? characterOrigin.x - NSMinX(bounds) : NSMaxX(bounds) - characterOrigin.x); + forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(horizontalDistance, (isNearerLeft ? .5 : 0.))); + + + /* How much will each callout rotate? No more than 1/8th. */ + HFASSERT(forbiddenAngle.length <= 1); + double changeInRotationPerCallout = fmin(.125, (1. - forbiddenAngle.length) / calloutCount); + double totalConsumedAmount = changeInRotationPerCallout * calloutCount; + + /* We would like to center around .375. */ + const double goalCenter = .375; + + /* We're going to pretend to work on a line segment that extends from the max prohibited angle all the way back to min */ + double segmentLength = 1. - forbiddenAngle.length; + double goalSegmentCenter = normalizeAngle(goalCenter - wedgeMax(forbiddenAngle)); //may exceed segmentLength! + + /* Now center us on the goal, or as close as we can get. */ + double consumedSegmentCenter; + + /* We only need to worry about wrapping around if we have some prohibited angle */ + if (forbiddenAngle.length <= 0) { //never expect < 0, but be paranoid + consumedSegmentCenter = goalSegmentCenter; + } else { + + /* The consumed segment center is confined to the segment range [amount/2, length - amount/2] */ + double consumedSegmentCenterMin = totalConsumedAmount/2; + double consumedSegmentCenterMax = segmentLength - totalConsumedAmount/2; + if (goalSegmentCenter >= consumedSegmentCenterMin && goalSegmentCenter < consumedSegmentCenterMax) { + /* We can hit our goal */ + consumedSegmentCenter = goalSegmentCenter; + } else { + /* Pick either the min or max location, depending on which one gets us closer to the goal segment center mod 1. */ + if (distanceMod1(goalSegmentCenter, consumedSegmentCenterMin) <= distanceMod1(goalSegmentCenter, consumedSegmentCenterMax)) { + consumedSegmentCenter = consumedSegmentCenterMin; + } else { + consumedSegmentCenter = consumedSegmentCenterMax; + } + + } + } + + /* Now convert this back to an angle */ + double consumedAngleCenter = normalizeAngle(wedgeMax(forbiddenAngle) + consumedSegmentCenter); + + // move us slightly towards the character + NSPoint teardropTipOrigin = NSMakePoint(characterOrigin.x + 1, characterOrigin.y + floor(lineHeight / 8.)); + + // make the pin + NSPoint pinStart, pinEnd; + pinStart = NSMakePoint(characterOrigin.x + .25, characterOrigin.y); + pinEnd = NSMakePoint(pinStart.x, pinStart.y + lineHeight); + + // store it all, invalidating as necessary + NSInteger i = 0; + FOREACH(HFRepresenterTextViewCallout *, callout, sharedCallouts) { + + /* Compute the rotation */ + double seq = (i+1)/2; //0, 1, -1, 2, -2... + if ((i & 1) == 0) seq = -seq; + //if we've got an even number of callouts, we want -.5, .5, -1.5, 1.5... + if (! (calloutCount & 1)) seq -= .5; + // compute the angle of rotation + double angle = consumedAngleCenter + seq * changeInRotationPerCallout; + // our notion of rotation has 0 meaning pointing right and going counterclockwise, but callouts with 0 pointing left and going clockwise, so convert + angle = normalizeAngle(.5 - angle); + + + NSRect beforeRect = [callout rect]; + + callout->rotation = angle; + callout->tipOrigin = teardropTipOrigin; + callout->pinStart = pinStart; + callout->pinEnd = pinEnd; + + // Only the first gets a pin + pinStart = pinEnd = NSZeroPoint; + + NSRect afterRect = [callout rect]; + + if (! NSEqualRects(beforeRect, afterRect)) { + [textView setNeedsDisplayInRect:beforeRect]; + [textView setNeedsDisplayInRect:afterRect]; + } + + i++; + } + + + /* We're done laying out these callouts */ + [remainingCallouts removeObjectsInArray:sharedCallouts]; + } + + [dropsPerByteLoc release]; +} + +- (CGAffineTransform)teardropTransform { + CGAffineTransform trans = CGAffineTransformMakeTranslation(tipOrigin.x, tipOrigin.y); + trans = CGAffineTransformRotate(trans, rotation * M_PI * 2); + return trans; +} + +- (NSRect)teardropBaseRect { + NSSize teardropSize = NSMakeSize(HFTeardropRadius * (1 + HFTeadropTipScale), HFTeardropRadius*2); + NSRect result = NSMakeRect(-teardropSize.width, -teardropSize.height/2, teardropSize.width, teardropSize.height); + return result; +} + +- (CGAffineTransform)shadowTransform { + CGFloat shadowXOffset = HFShadowXOffset; + CGFloat shadowYOffset = HFShadowYOffset; + CGFloat offscreenOffset = HFShadowOffscreenHack; + + // Figure out how much movement the shadow offset produces + CGFloat shadowTranslationDistance = hypot(shadowXOffset, shadowYOffset); + + CGAffineTransform transform = CGAffineTransformIdentity; + transform = CGAffineTransformTranslate(transform, tipOrigin.x + offscreenOffset - shadowXOffset, tipOrigin.y - shadowYOffset); + transform = CGAffineTransformRotate(transform, rotation * M_PI * 2 - atan2(shadowTranslationDistance, 2*HFTeardropRadius /* bulbHeight */)); + return transform; +} + +- (void)drawShadowWithClip:(NSRect)clip { + USE(clip); + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + + // Set the shadow. Note that these shadows are pretty unphysical for high rotations. + NSShadow *shadow = [[NSShadow alloc] init]; + [shadow setShadowBlurRadius:5.]; + [shadow setShadowOffset:NSMakeSize(HFShadowXOffset - HFShadowOffscreenHack, HFShadowYOffset)]; + [shadow setShadowColor:[NSColor colorWithDeviceWhite:0. alpha:.5]]; + [shadow set]; + [shadow release]; + + // Draw the shadow first and separately + CGAffineTransform transform = [self shadowTransform]; + CGContextConcatCTM(ctx, transform); + + NSBezierPath *teardrop = copyTeardropPath(); + [teardrop fill]; + [teardrop release]; + + // Clear the shadow + CGContextSetShadowWithColor(ctx, CGSizeZero, 0, NULL); + + // Undo the transform + CGContextConcatCTM(ctx, CGAffineTransformInvert(transform)); +} + +- (void)drawWithClip:(NSRect)clip { + USE(clip); + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + // Here's the font we'll use + CTFontRef ctfont = CTFontCreateWithName(CFSTR("Helvetica-Bold"), 1., NULL); + if (ctfont) { + // Set the font + [(NSFont *)ctfont set]; + + // Get characters + NSUInteger labelLength = MIN([_label length], kHFRepresenterTextViewCalloutMaxGlyphCount); + UniChar calloutUniLabel[kHFRepresenterTextViewCalloutMaxGlyphCount]; + [_label getCharacters:calloutUniLabel range:NSMakeRange(0, labelLength)]; + + // Get our glyphs and advances + CGGlyph glyphs[kHFRepresenterTextViewCalloutMaxGlyphCount]; + CGSize advances[kHFRepresenterTextViewCalloutMaxGlyphCount]; + CTFontGetGlyphsForCharacters(ctfont, calloutUniLabel, glyphs, labelLength); + CTFontGetAdvancesForGlyphs(ctfont, kCTFontHorizontalOrientation, glyphs, advances, labelLength); + + // Count our glyphs. Note: this won't work with any label containing spaces, etc. + NSUInteger glyphCount; + for (glyphCount = 0; glyphCount < labelLength; glyphCount++) { + if (glyphs[glyphCount] == 0) break; + } + + // Set our color. + [_color set]; + + // Draw the pin first + if (! NSEqualPoints(pinStart, pinEnd)) { + [NSBezierPath setDefaultLineWidth:1.25]; + [NSBezierPath strokeLineFromPoint:pinStart toPoint:pinEnd]; + } + + CGContextSaveGState(ctx); + CGContextBeginTransparencyLayerWithRect(ctx, NSRectToCGRect([self rect]), NULL); + + // Rotate and translate in preparation for drawing the teardrop + CGContextConcatCTM(ctx, [self teardropTransform]); + + // Draw the teardrop + NSBezierPath *teardrop = copyTeardropPath(); + [teardrop fill]; + [teardrop release]; + + // Draw the text with white and alpha. Use blend mode copy so that we clip out the shadow, and when the transparency layer is ended we'll composite over the text. + CGFloat textScale = (glyphCount == 1 ? 24 : 20); + + // we are flipped by default, so invert the rotation's sign to get the text direction. Use a little slop so we don't get jitter. + const CGFloat textDirection = (rotation <= .27 || rotation >= .73) ? -1 : 1; + + CGPoint positions[kHFRepresenterTextViewCalloutMaxGlyphCount]; + CGFloat totalAdvance = 0; + for (NSUInteger i=0; i < glyphCount; i++) { + // make sure to provide negative advances if necessary + positions[i].x = copysign(totalAdvance, -textDirection); + positions[i].y = 0; + CGFloat advance = advances[i].width; + // Workaround 5834794 + advance *= textScale; + // Tighten up the advances a little + advance *= .85; + totalAdvance += advance; + } + + + // Compute the vertical offset + CGFloat textYOffset = (glyphCount == 1 ? 4 : 5); + // LOL + if ([_label isEqualToString:@"6"]) textYOffset -= 1; + + + // Apply this text matrix + NSRect bulbRect = [self teardropBaseRect]; + CGAffineTransform textMatrix = CGAffineTransformMakeScale(-copysign(textScale, textDirection), copysign(textScale, textDirection)); //roughly the font size we want + textMatrix.tx = NSMinX(bulbRect) + HFTeardropRadius + copysign(totalAdvance/2, textDirection); + + + if (textDirection < 0) { + textMatrix.ty = NSMaxY(bulbRect) - textYOffset; + } else { + textMatrix.ty = NSMinY(bulbRect) + textYOffset; + } + + // Draw + CGContextSetTextMatrix(ctx, textMatrix); + CGContextSetTextDrawingMode(ctx, kCGTextClip); + CGContextShowGlyphsAtPositions(ctx, glyphs, positions, glyphCount); + + CGContextSetBlendMode(ctx, kCGBlendModeCopy); + CGContextSetGrayFillColor(ctx, 1., .66); //faint white fill + CGContextFillRect(ctx, NSRectToCGRect(NSInsetRect(bulbRect, -20, -20))); + + // Done drawing, so composite + CGContextEndTransparencyLayer(ctx); + CGContextRestoreGState(ctx); // this also restores the clip, which is important + + // Done with the font + CFRelease(ctfont); + } +} + +- (NSRect)rect { + // get the transformed teardrop rect + NSRect result = NSRectFromCGRect(CGRectApplyAffineTransform(NSRectToCGRect([self teardropBaseRect]), [self teardropTransform])); + + // outset a bit for the shadow + result = NSInsetRect(result, -8, -8); + return result; +} + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenterTextView_Internal.h b/bsnes/gb/HexFiend/HFRepresenterTextView_Internal.h new file mode 100644 index 00000000..70eed233 --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenterTextView_Internal.h @@ -0,0 +1,11 @@ +#import + +#define GLYPH_BUFFER_SIZE 16u + +@interface HFRepresenterTextView (HFInternal) + +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingLayoutManager:(NSLayoutManager *)textView glyphs:(CGGlyph *)glyphs; +- (NSUInteger)_glyphsForString:(NSString *)string withGeneratingTextView:(NSTextView *)textView glyphs:(CGGlyph *)glyphs; +- (NSUInteger)_getGlyphs:(CGGlyph *)glyphs forString:(NSString *)string font:(NSFont *)font; //uses CoreText. Here glyphs must have space for [string length] glyphs. + +@end diff --git a/bsnes/gb/HexFiend/HFRepresenter_Internal.h b/bsnes/gb/HexFiend/HFRepresenter_Internal.h new file mode 100644 index 00000000..9a0b704a --- /dev/null +++ b/bsnes/gb/HexFiend/HFRepresenter_Internal.h @@ -0,0 +1,7 @@ +#import + +@interface HFRepresenter (HFInternalStuff) + +- (void)_setController:(HFController *)controller; + +@end diff --git a/bsnes/gb/HexFiend/HFSharedMemoryByteSlice.h b/bsnes/gb/HexFiend/HFSharedMemoryByteSlice.h new file mode 100644 index 00000000..204492df --- /dev/null +++ b/bsnes/gb/HexFiend/HFSharedMemoryByteSlice.h @@ -0,0 +1,32 @@ +// +// HFSharedMemoryByteSlice.h +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFSharedMemoryByteSlice + @brief A subclass of HFByteSlice for working with data stored in memory. + + HFSharedMemoryByteSlice is a subclass of HFByteSlice that represents a portion of data from memory, e.g. typed or pasted in by the user. The term "shared" refers to the ability for mutiple HFSharedMemoryByteSlices to reference the same NSData; it does not mean that the data is in shared memory or shared between processes. + + Instances of HFSharedMemoryByteSlice are immutable (like all instances of HFByteSlice). However, to support efficient typing, the backing data is an instance of NSMutableData that may be grown. A referenced range of the NSMutableData will never have its contents changed, but it may be allowed to grow larger, so that the data does not have to be copied merely to append a single byte. This is implemented by overriding the -byteSliceByAppendingSlice: method of HFByteSlice. +*/ +@interface HFSharedMemoryByteSlice : HFByteSlice { + NSMutableData *data; + NSUInteger offset; + NSUInteger length; + unsigned char inlineTailLength; + unsigned char inlineTail[15]; //size chosen to exhaust padding of 32-byte allocator +} + +// copies the data +- (instancetype)initWithUnsharedData:(NSData *)data; + +// retains, does not copy +- (instancetype)initWithData:(NSMutableData *)data; +- (instancetype)initWithData:(NSMutableData *)data offset:(NSUInteger)offset length:(NSUInteger)length; + +@end diff --git a/bsnes/gb/HexFiend/HFSharedMemoryByteSlice.m b/bsnes/gb/HexFiend/HFSharedMemoryByteSlice.m new file mode 100644 index 00000000..fe7f43cc --- /dev/null +++ b/bsnes/gb/HexFiend/HFSharedMemoryByteSlice.m @@ -0,0 +1,209 @@ +// +// HFSharedMemoryByteSlice.m +// HexFiend_2 +// +// Copyright 2008 ridiculous_fish. All rights reserved. +// + +#import +#import + +#define MAX_FAST_PATH_SIZE (1 << 13) + +#define MAX_TAIL_LENGTH (sizeof ((HFSharedMemoryByteSlice *)NULL)->inlineTail / sizeof *((HFSharedMemoryByteSlice *)NULL)->inlineTail) + +@implementation HFSharedMemoryByteSlice + +- (instancetype)initWithUnsharedData:(NSData *)unsharedData { + self = [super init]; + REQUIRE_NOT_NULL(unsharedData); + NSUInteger dataLength = [unsharedData length]; + NSUInteger inlineAmount = MIN(dataLength, MAX_TAIL_LENGTH); + NSUInteger sharedAmount = dataLength - inlineAmount; + HFASSERT(inlineAmount <= UCHAR_MAX); + inlineTailLength = (unsigned char)inlineAmount; + length = sharedAmount; + if (inlineAmount > 0) { + [unsharedData getBytes:inlineTail range:NSMakeRange(dataLength - inlineAmount, inlineAmount)]; + } + if (sharedAmount > 0) { + data = [[NSMutableData alloc] initWithBytes:[unsharedData bytes] length:sharedAmount]; + } + return self; +} + +// retains, does not copy +- (instancetype)initWithData:(NSMutableData *)dat { + REQUIRE_NOT_NULL(dat); + return [self initWithData:dat offset:0 length:[dat length]]; +} + +- (instancetype)initWithData:(NSMutableData *)dat offset:(NSUInteger)off length:(NSUInteger)len { + self = [super init]; + REQUIRE_NOT_NULL(dat); + HFASSERT(off + len >= off); //check for overflow + HFASSERT(off + len <= [dat length]); + offset = off; + length = len; + data = [dat retain]; + return self; +} + +- (instancetype)initWithSharedData:(NSMutableData *)dat offset:(NSUInteger)off length:(NSUInteger)len tail:(const void *)tail tailLength:(NSUInteger)tailLen { + self = [super init]; + if (off || len) REQUIRE_NOT_NULL(dat); + if (tailLen) REQUIRE_NOT_NULL(tail); + HFASSERT(tailLen <= MAX_TAIL_LENGTH); + HFASSERT(off + len >= off); + HFASSERT(off + len <= [dat length]); + offset = off; + length = len; + data = [dat retain]; + HFASSERT(tailLen <= UCHAR_MAX); + inlineTailLength = (unsigned char)tailLen; + memcpy(inlineTail, tail, tailLen); + HFASSERT([self length] == tailLen + len); + return self; +} + +- (void)dealloc { + [data release]; + [super dealloc]; +} + +- (unsigned long long)length { + return length + inlineTailLength; +} + +- (void)copyBytes:(unsigned char *)dst range:(HFRange)lrange { + HFASSERT(HFSum(length, inlineTailLength) >= HFMaxRange(lrange)); + NSRange requestedRange = NSMakeRange(ll2l(lrange.location), ll2l(lrange.length)); + NSRange dataRange = NSMakeRange(0, length); + NSRange tailRange = NSMakeRange(length, inlineTailLength); + NSRange dataRangeToCopy = NSIntersectionRange(requestedRange, dataRange); + NSRange tailRangeToCopy = NSIntersectionRange(requestedRange, tailRange); + HFASSERT(HFSum(dataRangeToCopy.length, tailRangeToCopy.length) == lrange.length); + + if (dataRangeToCopy.length > 0) { + HFASSERT(HFSum(NSMaxRange(dataRangeToCopy), offset) <= [data length]); + const void *bytes = [data bytes]; + memcpy(dst, bytes + dataRangeToCopy.location + offset, dataRangeToCopy.length); + } + if (tailRangeToCopy.length > 0) { + HFASSERT(tailRangeToCopy.location >= length); + HFASSERT(NSMaxRange(tailRangeToCopy) - length <= inlineTailLength); + memcpy(dst + dataRangeToCopy.length, inlineTail + tailRangeToCopy.location - length, tailRangeToCopy.length); + } +} + +- (HFByteSlice *)subsliceWithRange:(HFRange)lrange { + if (HFRangeEqualsRange(lrange, HFRangeMake(0, HFSum(length, inlineTailLength)))) return [[self retain] autorelease]; + + HFByteSlice *result; + HFASSERT(lrange.length > 0); + HFASSERT(HFSum(length, inlineTailLength) >= HFMaxRange(lrange)); + NSRange requestedRange = NSMakeRange(ll2l(lrange.location), ll2l(lrange.length)); + NSRange dataRange = NSMakeRange(0, length); + NSRange tailRange = NSMakeRange(length, inlineTailLength); + NSRange dataRangeToCopy = NSIntersectionRange(requestedRange, dataRange); + NSRange tailRangeToCopy = NSIntersectionRange(requestedRange, tailRange); + HFASSERT(HFSum(dataRangeToCopy.length, tailRangeToCopy.length) == lrange.length); + + NSMutableData *resultData = NULL; + NSUInteger resultOffset = 0; + NSUInteger resultLength = 0; + const unsigned char *tail = NULL; + NSUInteger tailLength = 0; + if (dataRangeToCopy.length > 0) { + resultData = data; + HFASSERT(resultData != NULL); + resultOffset = offset + dataRangeToCopy.location; + resultLength = dataRangeToCopy.length; + HFASSERT(HFSum(resultOffset, resultLength) <= [data length]); + } + if (tailRangeToCopy.length > 0) { + tail = inlineTail + tailRangeToCopy.location - length; + tailLength = tailRangeToCopy.length; + HFASSERT(tail >= inlineTail && tail + tailLength <= inlineTail + inlineTailLength); + } + HFASSERT(resultLength + tailLength == lrange.length); + result = [[[[self class] alloc] initWithSharedData:resultData offset:resultOffset length:resultLength tail:tail tailLength:tailLength] autorelease]; + HFASSERT([result length] == lrange.length); + return result; +} + +- (HFByteSlice *)byteSliceByAppendingSlice:(HFByteSlice *)slice { + REQUIRE_NOT_NULL(slice); + const unsigned long long sliceLength = [slice length]; + if (sliceLength == 0) return self; + + const unsigned long long thisLength = [self length]; + + HFASSERT(inlineTailLength <= MAX_TAIL_LENGTH); + NSUInteger spaceRemainingInTail = MAX_TAIL_LENGTH - inlineTailLength; + + if (sliceLength <= spaceRemainingInTail) { + /* We can do our work entirely within the tail */ + NSUInteger newTailLength = (NSUInteger)sliceLength + inlineTailLength; + unsigned char newTail[MAX_TAIL_LENGTH]; + memcpy(newTail, inlineTail, inlineTailLength); + [slice copyBytes:newTail + inlineTailLength range:HFRangeMake(0, sliceLength)]; + HFByteSlice *result = [[[[self class] alloc] initWithSharedData:data offset:offset length:length tail:newTail tailLength:newTailLength] autorelease]; + HFASSERT([result length] == HFSum(sliceLength, thisLength)); + return result; + } + else { + /* We can't do our work entirely in the tail; see if we can append some shared data. */ + HFASSERT(offset + length >= offset); + if (offset + length == [data length]) { + /* We can append some shared data. But impose some reasonable limit on how big our slice can get; this is 16 MB */ + if (HFSum(thisLength, sliceLength) < (1ULL << 24)) { + NSUInteger newDataOffset = offset; + NSUInteger newDataLength = length; + unsigned char newDataTail[MAX_TAIL_LENGTH]; + unsigned char newDataTailLength = MAX_TAIL_LENGTH; + NSMutableData *newData = (data ? data : [[[NSMutableData alloc] init] autorelease]); + + NSUInteger sliceLengthInt = ll2l(sliceLength); + NSUInteger newTotalTailLength = sliceLengthInt + inlineTailLength; + HFASSERT(newTotalTailLength >= MAX_TAIL_LENGTH); + NSUInteger amountToShiftIntoSharedData = newTotalTailLength - MAX_TAIL_LENGTH; + NSUInteger amountToShiftIntoSharedDataFromTail = MIN(amountToShiftIntoSharedData, inlineTailLength); + NSUInteger amountToShiftIntoSharedDataFromNewSlice = amountToShiftIntoSharedData - amountToShiftIntoSharedDataFromTail; + + if (amountToShiftIntoSharedDataFromTail > 0) { + HFASSERT(amountToShiftIntoSharedDataFromTail <= inlineTailLength); + [newData appendBytes:inlineTail length:amountToShiftIntoSharedDataFromTail]; + newDataLength += amountToShiftIntoSharedDataFromTail; + } + if (amountToShiftIntoSharedDataFromNewSlice > 0) { + HFASSERT(amountToShiftIntoSharedDataFromNewSlice <= [slice length]); + NSUInteger dataLength = offset + length + amountToShiftIntoSharedDataFromTail; + HFASSERT([newData length] == dataLength); + [newData setLength:dataLength + amountToShiftIntoSharedDataFromNewSlice]; + [slice copyBytes:[newData mutableBytes] + dataLength range:HFRangeMake(0, amountToShiftIntoSharedDataFromNewSlice)]; + newDataLength += amountToShiftIntoSharedDataFromNewSlice; + } + + /* We've updated our data; now figure out the tail */ + NSUInteger amountOfTailFromNewSlice = sliceLengthInt - amountToShiftIntoSharedDataFromNewSlice; + HFASSERT(amountOfTailFromNewSlice <= MAX_TAIL_LENGTH); + [slice copyBytes:newDataTail + MAX_TAIL_LENGTH - amountOfTailFromNewSlice range:HFRangeMake(sliceLengthInt - amountOfTailFromNewSlice, amountOfTailFromNewSlice)]; + + /* Copy the rest, if any, from the end of self */ + NSUInteger amountOfTailFromSelf = MAX_TAIL_LENGTH - amountOfTailFromNewSlice; + HFASSERT(amountOfTailFromSelf <= inlineTailLength); + if (amountOfTailFromSelf > 0) { + memcpy(newDataTail, inlineTail + inlineTailLength - amountOfTailFromSelf, amountOfTailFromSelf); + } + + HFByteSlice *result = [[[[self class] alloc] initWithSharedData:newData offset:newDataOffset length:newDataLength tail:newDataTail tailLength:newDataTailLength] autorelease]; + HFASSERT([result length] == HFSum([slice length], [self length])); + return result; + } + } + } + return nil; +} + +@end diff --git a/bsnes/gb/HexFiend/HFStatusBarRepresenter.h b/bsnes/gb/HexFiend/HFStatusBarRepresenter.h new file mode 100644 index 00000000..e70b893f --- /dev/null +++ b/bsnes/gb/HexFiend/HFStatusBarRepresenter.h @@ -0,0 +1,31 @@ +// +// HFStatusBarRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @enum HFStatusBarMode + The HFStatusBarMode enum is used to describe the format of the byte counts displayed by the status bar. +*/ +typedef NS_ENUM(NSUInteger, HFStatusBarMode) { + HFStatusModeDecimal, ///< The status bar should display byte counts in decimal + HFStatusModeHexadecimal, ///< The status bar should display byte counts in hexadecimal + HFStatusModeApproximate, ///< The text should display byte counts approximately (e.g. "56.3 KB") + HFSTATUSMODECOUNT ///< The number of modes, to allow easy cycling +}; + +/*! @class HFStatusBarRepresenter + @brief The HFRepresenter for the status bar. + + HFStatusBarRepresenter is a subclass of HFRepresenter responsible for showing the status bar, which displays information like the total length of the document, or the number of selected bytes. +*/ +@interface HFStatusBarRepresenter : HFRepresenter { + HFStatusBarMode statusMode; +} + +@property (nonatomic) HFStatusBarMode statusMode; + +@end diff --git a/bsnes/gb/HexFiend/HFStatusBarRepresenter.m b/bsnes/gb/HexFiend/HFStatusBarRepresenter.m new file mode 100644 index 00000000..883677f9 --- /dev/null +++ b/bsnes/gb/HexFiend/HFStatusBarRepresenter.m @@ -0,0 +1,240 @@ +// +// HFStatusBarRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +#define kHFStatusBarDefaultModeUserDefaultsKey @"HFStatusBarDefaultMode" + +@interface HFStatusBarView : NSView { + NSCell *cell; + NSSize cellSize; + HFStatusBarRepresenter *representer; + NSDictionary *cellAttributes; + BOOL registeredForAppNotifications; +} + +- (void)setRepresenter:(HFStatusBarRepresenter *)rep; +- (void)setString:(NSString *)string; + +@end + + +@implementation HFStatusBarView + +- (void)_sharedInitStatusBarView { + NSMutableParagraphStyle *style = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease]; + [style setAlignment:NSCenterTextAlignment]; + cellAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSColor windowFrameTextColor], NSForegroundColorAttributeName, [NSFont labelFontOfSize:[NSFont smallSystemFontSize]], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]; + cell = [[NSCell alloc] initTextCell:@""]; + [cell setAlignment:NSCenterTextAlignment]; + [cell setBackgroundStyle:NSBackgroundStyleRaised]; +} + +- (instancetype)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + [self _sharedInitStatusBarView]; + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + [self _sharedInitStatusBarView]; + return self; +} + +// nothing to do in encodeWithCoder + +- (BOOL)isFlipped { return YES; } + +- (void)setRepresenter:(HFStatusBarRepresenter *)rep { + representer = rep; +} + +- (void)setString:(NSString *)string { + [cell setAttributedStringValue:[[[NSAttributedString alloc] initWithString:string attributes:cellAttributes] autorelease]]; + cellSize = [cell cellSize]; + [self setNeedsDisplay:YES]; +} + +- (void)drawRect:(NSRect)clip { + USE(clip); + NSRect bounds = [self bounds]; + // [[NSColor colorWithCalibratedWhite:(CGFloat).91 alpha:1] set]; + // NSRectFill(clip); + + + NSRect cellRect = NSMakeRect(NSMinX(bounds), HFCeil(NSMidY(bounds) - cellSize.height / 2), NSWidth(bounds), cellSize.height); + [cell drawWithFrame:cellRect inView:self]; +} + +- (void)setFrame:(NSRect)frame +{ + [super setFrame:frame]; + [self.window setContentBorderThickness:frame.origin.y + frame.size.height forEdge:NSMinYEdge]; +} + + +- (void)mouseDown:(NSEvent *)event { + USE(event); + HFStatusBarMode newMode = ([representer statusMode] + 1) % HFSTATUSMODECOUNT; + [representer setStatusMode:newMode]; + [[NSUserDefaults standardUserDefaults] setInteger:newMode forKey:kHFStatusBarDefaultModeUserDefaultsKey]; +} + +- (void)windowDidChangeKeyStatus:(NSNotification *)note { + USE(note); + [self setNeedsDisplay:YES]; +} + +- (void)viewDidMoveToWindow { + HFRegisterViewForWindowAppearanceChanges(self, @selector(windowDidChangeKeyStatus:), !registeredForAppNotifications); + registeredForAppNotifications = YES; + [self.window setContentBorderThickness:self.frame.origin.y + self.frame.size.height forEdge:NSMinYEdge]; + [super viewDidMoveToWindow]; +} + +- (void)viewWillMoveToWindow:(NSWindow *)newWindow { + HFUnregisterViewForWindowAppearanceChanges(self, NO); + [super viewWillMoveToWindow:newWindow]; +} + +- (void)dealloc { + HFUnregisterViewForWindowAppearanceChanges(self, registeredForAppNotifications); + [cell release]; + [cellAttributes release]; + [super dealloc]; +} + +@end + +@implementation HFStatusBarRepresenter + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeInt64:statusMode forKey:@"HFStatusMode"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + statusMode = (NSUInteger)[coder decodeInt64ForKey:@"HFStatusMode"]; + return self; +} + +- (instancetype)init { + self = [super init]; + statusMode = [[NSUserDefaults standardUserDefaults] integerForKey:kHFStatusBarDefaultModeUserDefaultsKey]; + return self; +} + +- (NSView *)createView { + HFStatusBarView *view = [[HFStatusBarView alloc] initWithFrame:NSMakeRect(0, 0, 100, 18)]; + [view setRepresenter:self]; + [view setAutoresizingMask:NSViewWidthSizable]; + return view; +} + +- (NSString *)describeLength:(unsigned long long)length { + switch (statusMode) { + case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu byte%s", length, length == 1 ? "" : "s"]; + case HFStatusModeHexadecimal: return [NSString stringWithFormat:@"0x%llX byte%s", length, length == 1 ? "" : "s"]; + case HFStatusModeApproximate: return [NSString stringWithFormat:@"%@", HFDescribeByteCount(length)]; + default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; + } +} + +- (NSString *)describeOffset:(unsigned long long)offset { + switch (statusMode) { + case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu", offset]; + case HFStatusModeHexadecimal: return [NSString stringWithFormat:@"0x%llX", offset]; + case HFStatusModeApproximate: return [NSString stringWithFormat:@"%@", HFDescribeByteCount(offset)]; + default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; + } +} + +/* same as describeOffset, except we treat Approximate like Hexadecimal */ +- (NSString *)describeOffsetExcludingApproximate:(unsigned long long)offset { + switch (statusMode) { + case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu", offset]; + case HFStatusModeHexadecimal: + case HFStatusModeApproximate: return [NSString stringWithFormat:@"0x%llX", offset]; + default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; + } +} + +- (NSString *)stringForEmptySelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length { + return [NSString stringWithFormat:@"%@ out of %@", [self describeOffset:offset], [self describeLength:length]]; +} + +- (NSString *)stringForSingleByteSelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length { + return [NSString stringWithFormat:@"Byte %@ selected out of %@", [self describeOffset:offset], [self describeLength:length]]; +} + +- (NSString *)stringForSingleRangeSelection:(HFRange)range length:(unsigned long long)length { + return [NSString stringWithFormat:@"%@ selected at offset %@ out of %@", [self describeLength:range.length], [self describeOffsetExcludingApproximate:range.location], [self describeLength:length]]; +} + +- (NSString *)stringForMultipleSelectionsWithLength:(unsigned long long)multipleSelectionLength length:(unsigned long long)length { + return [NSString stringWithFormat:@"%@ selected at multiple offsets out of %@", [self describeLength:multipleSelectionLength], [self describeLength:length]]; +} + + +- (void)updateString { + NSString *string = nil; + HFController *controller = [self controller]; + if (controller) { + unsigned long long length = [controller contentsLength]; + NSArray *ranges = [controller selectedContentsRanges]; + NSUInteger rangeCount = [ranges count]; + if (rangeCount == 1) { + HFRange range = [ranges[0] HFRange]; + if (range.length == 0) { + string = [self stringForEmptySelectionAtOffset:range.location length:length]; + } + else if (range.length == 1) { + string = [self stringForSingleByteSelectionAtOffset:range.location length:length]; + } + else { + string = [self stringForSingleRangeSelection:range length:length]; + } + } + else { + unsigned long long totalSelectionLength = 0; + FOREACH(HFRangeWrapper *, wrapper, ranges) { + HFRange range = [wrapper HFRange]; + totalSelectionLength = HFSum(totalSelectionLength, range.length); + } + string = [self stringForMultipleSelectionsWithLength:totalSelectionLength length:length]; + } + } + if (! string) string = @""; + [[self view] setString:string]; +} + +- (HFStatusBarMode)statusMode { + return statusMode; +} + +- (void)setStatusMode:(HFStatusBarMode)mode { + statusMode = mode; + [self updateString]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & (HFControllerContentLength | HFControllerSelectedRanges)) { + [self updateString]; + } +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(0, -1); +} + +@end diff --git a/bsnes/gb/HexFiend/HFStringEncodingTextRepresenter.h b/bsnes/gb/HexFiend/HFStringEncodingTextRepresenter.h new file mode 100644 index 00000000..2c5da7b5 --- /dev/null +++ b/bsnes/gb/HexFiend/HFStringEncodingTextRepresenter.h @@ -0,0 +1,26 @@ +// +// HFASCIITextRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFStringEncodingTextRepresenter + + @brief An HFRepresenter responsible for showing data interpreted via an NSStringEncoding. + + HFHexTextRepresenter is an HFRepresenter responsible for showing and editing data interpreted via an NSStringEncoding. Currently only supersets of ASCII are supported. +*/ +@interface HFStringEncodingTextRepresenter : HFTextRepresenter { + NSStringEncoding stringEncoding; + +} + +/*! Get the string encoding for this representer. The default encoding is [NSString defaultCStringEncoding]. */ +@property (nonatomic) NSStringEncoding encoding; + +/*! Set the string encoding for this representer. */ + +@end diff --git a/bsnes/gb/HexFiend/HFStringEncodingTextRepresenter.m b/bsnes/gb/HexFiend/HFStringEncodingTextRepresenter.m new file mode 100644 index 00000000..27ea995c --- /dev/null +++ b/bsnes/gb/HexFiend/HFStringEncodingTextRepresenter.m @@ -0,0 +1,121 @@ +// +// HFASCIITextRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +@interface HFStringEncodingPasteboardOwner : HFPasteboardOwner { + NSStringEncoding encoding; +} +@property (nonatomic) NSStringEncoding encoding; +@end + +@implementation HFStringEncodingPasteboardOwner +- (void)setEncoding:(NSStringEncoding)val { encoding = val; } +- (NSStringEncoding)encoding { return encoding; } + +- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker { + HFASSERT([type isEqual:NSStringPboardType]); + HFByteArray *byteArray = [self byteArray]; + HFASSERT(length <= NSUIntegerMax); + NSUInteger dataLength = ll2l(length); + NSUInteger stringLength = dataLength; + NSUInteger offset = 0, remaining = dataLength; + unsigned char * restrict const stringBuffer = check_malloc(stringLength); + while (remaining > 0) { + NSUInteger amountToCopy = MIN(32u * 1024u, remaining); + [byteArray copyBytes:stringBuffer + offset range:HFRangeMake(offset, amountToCopy)]; + offset += amountToCopy; + remaining -= amountToCopy; + } + NSString *string = [[NSString alloc] initWithBytesNoCopy:stringBuffer length:stringLength encoding:encoding freeWhenDone:YES]; + [pboard setString:string forType:type]; + [string release]; +} + +- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength { + return dataLength; +} + +@end + +@implementation HFStringEncodingTextRepresenter + +- (instancetype)init { + self = [super init]; + stringEncoding = [NSString defaultCStringEncoding]; + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + stringEncoding = (NSStringEncoding)[coder decodeInt64ForKey:@"HFStringEncoding"]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeInt64:stringEncoding forKey:@"HFStringEncoding"]; +} + +- (Class)_textViewClass { + return [HFRepresenterStringEncodingTextView class]; +} + +- (NSStringEncoding)encoding { + return stringEncoding; +} + +- (void)setEncoding:(NSStringEncoding)encoding { + stringEncoding = encoding; + [[self view] setEncoding:encoding]; + [[self controller] representer:self changedProperties:HFControllerViewSizeRatios]; +} + +- (void)initializeView { + [[self view] setEncoding:stringEncoding]; + [super initializeView]; +} + +- (void)insertText:(NSString *)text { + REQUIRE_NOT_NULL(text); + NSData *data = [text dataUsingEncoding:[self encoding] allowLossyConversion:NO]; + if (! data) { + NSBeep(); + } + else if ([data length]) { // a 0 length text can come about via e.g. option-e + [[self controller] insertData:data replacingPreviousBytes:0 allowUndoCoalescing:YES]; + } +} + +- (NSData *)dataFromPasteboardString:(NSString *)string { + REQUIRE_NOT_NULL(string); + return [string dataUsingEncoding:[self encoding] allowLossyConversion:NO]; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(1, 0); +} + +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + HFByteArray *selection = [[self controller] byteArrayForSelectedContentsRanges]; + HFASSERT(selection != NULL); + if ([selection length] == 0) { + NSBeep(); + } + else { + HFStringEncodingPasteboardOwner *owner = [HFStringEncodingPasteboardOwner ownPasteboard:pb forByteArray:selection withTypes:@[HFPrivateByteArrayPboardType, NSStringPboardType]]; + [owner setEncoding:[self encoding]]; + [owner setBytesPerLine:[self bytesPerLine]]; + } +} + +@end diff --git a/bsnes/gb/HexFiend/HFTextRepresenter.h b/bsnes/gb/HexFiend/HFTextRepresenter.h new file mode 100644 index 00000000..306a197f --- /dev/null +++ b/bsnes/gb/HexFiend/HFTextRepresenter.h @@ -0,0 +1,39 @@ +// +// HFTextRepresenter.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import + +/*! @class HFTextRepresenter + @brief An HFRepresenter that draws text (e.g. the hex or ASCII view). + + HFTextRepresenter is an abstract subclass of HFRepresenter that is responsible for displaying text. There are two concrete subclass, HFHexTextRepresenter and HFStringEncodingTextRepresenter. + + Most of the functionality of HFTextRepresenter is private, and there is not yet enough exposed to allow creating new representers based on it. However, there is a small amount of configurability. +*/ +@interface HFTextRepresenter : HFRepresenter {} +/*! Given a rect edge, return an NSRect representing the maximum edge in that direction, in the coordinate system of the receiver's view. The dimension in the direction of the edge is 0 (so if edge is NSMaxXEdge, the resulting width is 0). The returned rect is in the coordinate space of the receiver's view. If the byte range is not displayed, returns NSZeroRect. + + If range is entirely above the visible region, returns an NSRect whose width and height are 0, and whose origin is -CGFLOAT_MAX (the most negative CGFloat). If range is entirely below the visible region, returns the same except with CGFLOAT_MAX (positive). + + This raises an exception if range is empty. +*/ +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forByteRange:(HFRange)range; + +/*! Returns the origin of the character at the given byte index. The returned point is in the coordinate space of the receiver's view. If the character is not displayed because it would be above the displayed range, returns {0, -CGFLOAT_MAX}. If it is not displayed because it is below the displayed range, returns {0, CGFLOAT_MAX}. As a special affordance, you may pass a byte index one greater than the contents length of the controller, and it will return the result as if the byte existed. + */ +- (NSPoint)locationOfCharacterAtByteIndex:(unsigned long long)byteIndex; + +/*! The per-row background colors. Each row is drawn with the next color in turn, cycling back to the beginning when the array is exhausted. Any empty space is filled with the first color in the array. If the array is empty, then the background is drawn with \c clearColor. + */ +@property (nonatomic, copy) NSArray *rowBackgroundColors; + +/*! Whether the text view behaves like a text field (YES) or a text view (NO). Currently this determines whether it draws a focus ring when it is the first responder. +*/ +@property (nonatomic) BOOL behavesAsTextField; + +@end diff --git a/bsnes/gb/HexFiend/HFTextRepresenter.m b/bsnes/gb/HexFiend/HFTextRepresenter.m new file mode 100644 index 00000000..b8793b45 --- /dev/null +++ b/bsnes/gb/HexFiend/HFTextRepresenter.m @@ -0,0 +1,377 @@ +// +// HFTextRepresenter.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import +#import +#import + +@implementation HFTextRepresenter + +- (Class)_textViewClass { + UNIMPLEMENTED(); +} + +- (instancetype)init { + self = [super init]; + + if (@available(macOS 10.14, *)) { + _rowBackgroundColors = [[NSColor alternatingContentBackgroundColors] retain]; + } else { + NSColor *color1 = [NSColor windowBackgroundColor]; + NSColor *color2 = [NSColor colorWithDeviceWhite:0.96 alpha:1]; + _rowBackgroundColors = [@[color1, color2] retain]; + } + + return self; +} + +- (void)dealloc { + if ([self isViewLoaded]) { + [[self view] clearRepresenter]; + } + [_rowBackgroundColors release]; + [super dealloc]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + [super encodeWithCoder:coder]; + [coder encodeBool:_behavesAsTextField forKey:@"HFBehavesAsTextField"]; + [coder encodeObject:_rowBackgroundColors forKey:@"HFRowBackgroundColors"]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + HFASSERT([coder allowsKeyedCoding]); + self = [super initWithCoder:coder]; + _behavesAsTextField = [coder decodeBoolForKey:@"HFBehavesAsTextField"]; + _rowBackgroundColors = [[coder decodeObjectForKey:@"HFRowBackgroundColors"] retain]; + return self; +} + +- (NSView *)createView { + HFRepresenterTextView *view = [[[self _textViewClass] alloc] initWithRepresenter:self]; + [view setAutoresizingMask:NSViewHeightSizable]; + return view; +} + +- (HFByteArrayDataStringType)byteArrayDataStringType { + UNIMPLEMENTED(); +} + +- (HFRange)entireDisplayedRange { + HFController *controller = [self controller]; + unsigned long long contentsLength = [controller contentsLength]; + HFASSERT(controller != NULL); + HFFPRange displayedLineRange = [controller displayedLineRange]; + NSUInteger bytesPerLine = [controller bytesPerLine]; + unsigned long long lineStart = HFFPToUL(floorl(displayedLineRange.location)); + unsigned long long lineEnd = HFFPToUL(ceill(displayedLineRange.location + displayedLineRange.length)); + HFASSERT(lineEnd >= lineStart); + HFRange byteRange = HFRangeMake(HFProductULL(bytesPerLine, lineStart), HFProductULL(lineEnd - lineStart, bytesPerLine)); + if (byteRange.length == 0) { + /* This can happen if we are too small to even show one line */ + return HFRangeMake(0, 0); + } + else { + HFASSERT(byteRange.location <= contentsLength); + byteRange.length = MIN(byteRange.length, contentsLength - byteRange.location); + HFASSERT(HFRangeIsSubrangeOfRange(byteRange, HFRangeMake(0, [controller contentsLength]))); + return byteRange; + } +} + +- (NSRect)furthestRectOnEdge:(NSRectEdge)edge forByteRange:(HFRange)byteRange { + HFASSERT(byteRange.length > 0); + HFRange displayedRange = [self entireDisplayedRange]; + HFRange intersection = HFIntersectionRange(displayedRange, byteRange); + NSRect result = {{0,},}; + if (intersection.length > 0) { + NSRange intersectionNSRange = NSMakeRange(ll2l(intersection.location - displayedRange.location), ll2l(intersection.length)); + if (intersectionNSRange.length > 0) { + result = [[self view] furthestRectOnEdge:edge forRange:intersectionNSRange]; + } + } + else if (byteRange.location < displayedRange.location) { + /* We're below it. */ + return NSMakeRect(-CGFLOAT_MAX, -CGFLOAT_MAX, 0, 0); + } + else if (byteRange.location >= HFMaxRange(displayedRange)) { + /* We're above it */ + return NSMakeRect(CGFLOAT_MAX, CGFLOAT_MAX, 0, 0); + } + else { + /* Shouldn't be possible to get here */ + [NSException raise:NSInternalInconsistencyException format:@"furthestRectOnEdge: expected an intersection, or a range below or above the byte range, but nothin'"]; + } + return result; +} + +- (NSPoint)locationOfCharacterAtByteIndex:(unsigned long long)index { + NSPoint result; + HFRange displayedRange = [self entireDisplayedRange]; + if (HFLocationInRange(index, displayedRange) || index == HFMaxRange(displayedRange)) { + NSUInteger location = ll2l(index - displayedRange.location); + result = [[self view] originForCharacterAtByteIndex:location]; + } + else if (index < displayedRange.location) { + result = NSMakePoint(-CGFLOAT_MAX, -CGFLOAT_MAX); + } + else { + result = NSMakePoint(CGFLOAT_MAX, CGFLOAT_MAX); + } + return result; +} + +- (HFTextVisualStyleRun *)styleForAttributes:(NSSet *)attributes range:(NSRange)range { + HFTextVisualStyleRun *run = [[[HFTextVisualStyleRun alloc] init] autorelease]; + [run setRange:range]; + [run setForegroundColor:[NSColor blackColor]]; + + return run; +} + +- (NSArray *)stylesForRange:(HFRange)range { + return nil; +} + +- (void)updateText { + HFController *controller = [self controller]; + HFRepresenterTextView *view = [self view]; + HFRange entireDisplayedRange = [self entireDisplayedRange]; + [view setData:[controller dataForRange:entireDisplayedRange]]; + [view setStyles:[self stylesForRange:entireDisplayedRange]]; + HFFPRange lineRange = [controller displayedLineRange]; + long double offsetLongDouble = lineRange.location - floorl(lineRange.location); + CGFloat offset = ld2f(offsetLongDouble); + [view setVerticalOffset:offset]; + [view setStartingLineBackgroundColorIndex:ll2l(HFFPToUL(floorl(lineRange.location)) % NSUIntegerMax)]; +} + +- (void)initializeView { + [super initializeView]; + HFRepresenterTextView *view = [self view]; + HFController *controller = [self controller]; + if (controller) { + [view setFont:[controller font]]; + [view setEditable:[controller editable]]; + [self updateText]; + } + else { + [view setFont:[NSFont fontWithName:HFDEFAULT_FONT size:HFDEFAULT_FONTSIZE]]; + } +} + +- (void)scrollWheel:(NSEvent *)event { + [[self controller] scrollWithScrollEvent:event]; +} + +- (void)selectAll:(id)sender { + [[self controller] selectAll:sender]; +} + +- (double)selectionPulseAmount { + return [[self controller] selectionPulseAmount]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & (HFControllerFont | HFControllerLineHeight)) { + [[self view] setFont:[[self controller] font]]; + } + if (bits & (HFControllerContentValue | HFControllerDisplayedLineRange | HFControllerByteRangeAttributes)) { + [self updateText]; + } + if (bits & (HFControllerSelectedRanges | HFControllerDisplayedLineRange)) { + [[self view] updateSelectedRanges]; + } + if (bits & (HFControllerEditable)) { + [[self view] setEditable:[[self controller] editable]]; + } + if (bits & (HFControllerAntialias)) { + [[self view] setShouldAntialias:[[self controller] shouldAntialias]]; + } + if (bits & (HFControllerShowCallouts)) { + [[self view] setShouldDrawCallouts:[[self controller] shouldShowCallouts]]; + } + if (bits & (HFControllerColorBytes)) { + if([[self controller] shouldColorBytes]) { + [[self view] setByteColoring: ^(uint8_t byte, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a){ + *r = *g = *b = (uint8_t)(255 * ((255-byte)/255.0*0.6+0.4)); + *a = (uint8_t)(255 * 0.7); + }]; + } else { + [[self view] setByteColoring:NULL]; + } + } + [super controllerDidChange:bits]; +} + +- (double)maximumAvailableLinesForViewHeight:(CGFloat)viewHeight { + return [[self view] maximumAvailableLinesForViewHeight:viewHeight]; +} + +- (NSUInteger)maximumBytesPerLineForViewWidth:(CGFloat)viewWidth { + return [[self view] maximumBytesPerLineForViewWidth:viewWidth]; +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + return [[self view] minimumViewWidthForBytesPerLine:bytesPerLine]; +} + +- (NSUInteger)byteGranularity { + HFRepresenterTextView *view = [self view]; + NSUInteger bytesPerColumn = MAX([view bytesPerColumn], 1u), bytesPerCharacter = [view bytesPerCharacter]; + return HFLeastCommonMultiple(bytesPerColumn, bytesPerCharacter); +} + +- (NSArray *)displayedSelectedContentsRanges { + HFController *controller = [self controller]; + NSArray *result; + NSArray *selectedRanges = [controller selectedContentsRanges]; + HFRange displayedRange = [self entireDisplayedRange]; + + HFASSERT(displayedRange.length <= NSUIntegerMax); + NEW_ARRAY(NSValue *, clippedSelectedRanges, [selectedRanges count]); + NSUInteger clippedRangeIndex = 0; + FOREACH(HFRangeWrapper *, wrapper, selectedRanges) { + HFRange selectedRange = [wrapper HFRange]; + BOOL clippedRangeIsVisible; + NSRange clippedSelectedRange = {0,}; + /* Necessary because zero length ranges do not intersect anything */ + if (selectedRange.length == 0) { + /* Remember that {6, 0} is considered a subrange of {3, 3} */ + clippedRangeIsVisible = HFRangeIsSubrangeOfRange(selectedRange, displayedRange); + if (clippedRangeIsVisible) { + HFASSERT(selectedRange.location >= displayedRange.location); + clippedSelectedRange.location = ll2l(selectedRange.location - displayedRange.location); + clippedSelectedRange.length = 0; + } + } + else { + // selectedRange.length > 0 + clippedRangeIsVisible = HFIntersectsRange(selectedRange, displayedRange); + if (clippedRangeIsVisible) { + HFRange intersectionRange = HFIntersectionRange(selectedRange, displayedRange); + HFASSERT(intersectionRange.location >= displayedRange.location); + clippedSelectedRange.location = ll2l(intersectionRange.location - displayedRange.location); + clippedSelectedRange.length = ll2l(intersectionRange.length); + } + } + if (clippedRangeIsVisible) clippedSelectedRanges[clippedRangeIndex++] = [NSValue valueWithRange:clippedSelectedRange]; + } + result = [NSArray arrayWithObjects:clippedSelectedRanges count:clippedRangeIndex]; + FREE_ARRAY(clippedSelectedRanges); + return result; +} + +//maps bookmark keys as NSNumber to byte locations as NSNumbers. Because bookmark callouts may extend beyond the lines containing them, allow a larger range by 10 lines. +- (NSDictionary *)displayedBookmarkLocations { + NSMutableDictionary *result = nil; + HFController *controller = [self controller]; + NSUInteger rangeExtension = 10 * [controller bytesPerLine]; + HFRange displayedRange = [self entireDisplayedRange]; + + HFRange includedRange = displayedRange; + + /* Extend the bottom */ + unsigned long long bottomExtension = MIN(includedRange.location, rangeExtension); + includedRange.location -= bottomExtension; + includedRange.length += bottomExtension; + + /* Extend the top */ + unsigned long long topExtension = MIN([controller contentsLength] - HFMaxRange(includedRange), rangeExtension); + includedRange.length = HFSum(includedRange.length, topExtension); + + return result; +} + +- (unsigned long long)byteIndexForCharacterIndex:(NSUInteger)characterIndex { + HFController *controller = [self controller]; + HFFPRange lineRange = [controller displayedLineRange]; + unsigned long long scrollAmount = HFFPToUL(floorl(lineRange.location)); + unsigned long long byteIndex = HFProductULL(scrollAmount, [controller bytesPerLine]) + characterIndex * [[self view] bytesPerCharacter]; + return byteIndex; +} + +- (void)beginSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex { + [[self controller] beginSelectionWithEvent:event forByteIndex:[self byteIndexForCharacterIndex:characterIndex]]; +} + +- (void)continueSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex { + [[self controller] continueSelectionWithEvent:event forByteIndex:[self byteIndexForCharacterIndex:characterIndex]]; +} + +- (void)endSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex { + [[self controller] endSelectionWithEvent:event forByteIndex:[self byteIndexForCharacterIndex:characterIndex]]; +} + +- (void)insertText:(NSString *)text { + USE(text); + UNIMPLEMENTED_VOID(); +} + +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb { + USE(pb); + UNIMPLEMENTED_VOID(); +} + +- (void)cutSelectedBytesToPasteboard:(NSPasteboard *)pb { + [self copySelectedBytesToPasteboard:pb]; + [[self controller] deleteSelection]; +} + +- (NSData *)dataFromPasteboardString:(NSString *)string { + USE(string); + UNIMPLEMENTED(); +} + +- (BOOL)canPasteFromPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + if ([[self controller] editable]) { + // we can paste if the pboard contains text or contains an HFByteArray + return [HFPasteboardOwner unpackByteArrayFromPasteboard:pb] || [pb availableTypeFromArray:@[NSStringPboardType]]; + } + return NO; +} + +- (BOOL)canCut { + /* We can cut if we are editable, we have at least one byte selected, and we are not in overwrite mode */ + HFController *controller = [self controller]; + if ([controller editMode] != HFInsertMode) return NO; + if (! [controller editable]) return NO; + + FOREACH(HFRangeWrapper *, rangeWrapper, [controller selectedContentsRanges]) { + if ([rangeWrapper HFRange].length > 0) return YES; //we have something selected + } + return NO; // we did not find anything selected +} + +- (BOOL)pasteBytesFromPasteboard:(NSPasteboard *)pb { + REQUIRE_NOT_NULL(pb); + BOOL result = NO; + HFByteArray *byteArray = [HFPasteboardOwner unpackByteArrayFromPasteboard:pb]; + if (byteArray) { + [[self controller] insertByteArray:byteArray replacingPreviousBytes:0 allowUndoCoalescing:NO]; + result = YES; + } + else { + NSString *stringType = [pb availableTypeFromArray:@[NSStringPboardType]]; + if (stringType) { + NSString *stringValue = [pb stringForType:stringType]; + if (stringValue) { + NSData *data = [self dataFromPasteboardString:stringValue]; + if (data) { + [[self controller] insertData:data replacingPreviousBytes:0 allowUndoCoalescing:NO]; + } + } + } + } + return result; +} + +@end diff --git a/bsnes/gb/HexFiend/HFTextRepresenter_Internal.h b/bsnes/gb/HexFiend/HFTextRepresenter_Internal.h new file mode 100644 index 00000000..c1e9f018 --- /dev/null +++ b/bsnes/gb/HexFiend/HFTextRepresenter_Internal.h @@ -0,0 +1,33 @@ +#import + +@interface HFTextRepresenter (HFInternal) + +- (NSArray *)displayedSelectedContentsRanges; //returns an array of NSValues representing the selected ranges (as NSRanges) clipped to the displayed range. + +- (NSDictionary *)displayedBookmarkLocations; //returns an dictionary mapping bookmark names to bookmark locations. Bookmark locations may be negative. + +- (void)beginSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex; +- (void)continueSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex; +- (void)endSelectionWithEvent:(NSEvent *)event forCharacterIndex:(NSUInteger)characterIndex; + +// Copy/Paste methods +- (void)copySelectedBytesToPasteboard:(NSPasteboard *)pb; +- (void)cutSelectedBytesToPasteboard:(NSPasteboard *)pb; +- (BOOL)canPasteFromPasteboard:(NSPasteboard *)pb; +- (BOOL)canCut; +- (BOOL)pasteBytesFromPasteboard:(NSPasteboard *)pb; + +// Must be implemented by subclasses +- (void)insertText:(NSString *)text; + +// Must be implemented by subclasses. Return NSData representing the string value. +- (NSData *)dataFromPasteboardString:(NSString *)string; + +// Value between [0, 1] +- (double)selectionPulseAmount; + +- (void)scrollWheel:(NSEvent *)event; + +- (void)selectAll:(id)sender; + +@end diff --git a/bsnes/gb/HexFiend/HFTextRepresenter_KeyBinding.m b/bsnes/gb/HexFiend/HFTextRepresenter_KeyBinding.m new file mode 100644 index 00000000..b6e16d69 --- /dev/null +++ b/bsnes/gb/HexFiend/HFTextRepresenter_KeyBinding.m @@ -0,0 +1,128 @@ +// +// HFTextRepresenter_KeyBinding.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import +#import +#import + +#define FORWARD(x) - (void)x : sender { USE(sender); UNIMPLEMENTED_VOID(); } + +@implementation HFTextRepresenter (HFKeyBinding) + +- (void)moveRight:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementByte andModifySelection:NO]; } +- (void)moveLeft:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementByte andModifySelection:NO]; } +- (void)moveUp:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementLine andModifySelection:NO]; } +- (void)moveDown:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementLine andModifySelection:NO]; } +- (void)moveWordRight:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementColumn andModifySelection:NO]; } +- (void)moveWordLeft:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementColumn andModifySelection:NO]; } + +- (void)moveRightAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementByte andModifySelection:YES]; } +- (void)moveLeftAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementByte andModifySelection:YES]; } +- (void)moveUpAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementLine andModifySelection:YES]; } +- (void)moveDownAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementLine andModifySelection:YES]; } +- (void)moveWordRightAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementColumn andModifySelection:YES]; } +- (void)moveWordLeftAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementColumn andModifySelection:YES]; } + +- (void)moveForward:unused { USE(unused); [self moveRight:unused]; } +- (void)moveBackward:unused { USE(unused); [self moveLeft:unused]; } + +- (void)moveWordForward:unused { USE(unused); [self moveWordRight:unused]; } +- (void)moveWordBackward:unused { USE(unused); [self moveWordLeft:unused]; } +- (void)moveForwardAndModifySelection:unused { USE(unused); [self moveRightAndModifySelection:unused]; } +- (void)moveBackwardAndModifySelection:unused { USE(unused); [self moveLeftAndModifySelection:unused]; } +- (void)moveWordForwardAndModifySelection:unused { USE(unused); [self moveForwardAndModifySelection:unused]; } +- (void)moveWordBackwardAndModifySelection:unused { USE(unused); [self moveBackwardAndModifySelection:unused]; } + +- (void)deleteBackward:unused { USE(unused); [[self controller] deleteDirection:HFControllerDirectionLeft]; } +- (void)deleteForward:unused { USE(unused); [[self controller] deleteDirection:HFControllerDirectionRight]; } +- (void)deleteWordForward:unused { USE(unused); [self deleteForward:unused]; } +- (void)deleteWordBackward:unused { USE(unused); [self deleteBackward:unused]; } + +- (void)delete:unused { USE(unused); [self deleteForward:unused]; } + + //todo: implement these + +- (void)deleteToBeginningOfLine:(id)sender { USE(sender); } +- (void)deleteToEndOfLine:(id)sender { USE(sender); } +- (void)deleteToBeginningOfParagraph:(id)sender { USE(sender); } +- (void)deleteToEndOfParagraph:(id)sender { USE(sender); } + +- (void)moveToBeginningOfLine:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionLeft andModifySelection:NO]; } +- (void)moveToEndOfLine:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionRight andModifySelection:NO]; } +- (void)moveToBeginningOfDocument:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementDocument andModifySelection:NO]; } +- (void)moveToEndOfDocument:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementDocument andModifySelection:NO]; } + +- (void)moveToBeginningOfLineAndModifySelection:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionLeft andModifySelection:YES]; } +- (void)moveToEndOfLineAndModifySelection:unused { USE(unused); [[self controller] moveToLineBoundaryInDirection:HFControllerDirectionRight andModifySelection:YES]; } +- (void)moveToBeginningOfDocumentAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionLeft withGranularity:HFControllerMovementDocument andModifySelection:YES]; } +- (void)moveToEndOfDocumentAndModifySelection:unused { USE(unused); [[self controller] moveInDirection:HFControllerDirectionRight withGranularity:HFControllerMovementDocument andModifySelection:YES]; } + +- (void)moveToBeginningOfParagraph:unused { USE(unused); [self moveToBeginningOfLine:unused]; } +- (void)moveToEndOfParagraph:unused { USE(unused); [self moveToEndOfLine:unused]; } +- (void)moveToBeginningOfParagraphAndModifySelection:unused { USE(unused); [self moveToBeginningOfLineAndModifySelection:unused]; } +- (void)moveToEndOfParagraphAndModifySelection:unused { USE(unused); [self moveToEndOfLineAndModifySelection:unused]; } + +- (void)scrollPageDown:unused { USE(unused); [[self controller] scrollByLines:[[self controller] displayedLineRange].length]; } +- (void)scrollPageUp:unused { USE(unused); [[self controller] scrollByLines: - [[self controller] displayedLineRange].length]; } +- (void)pageDown:unused { USE(unused); [self scrollPageDown:unused]; } +- (void)pageUp:unused { USE(unused); [self scrollPageUp:unused]; } + +- (void)centerSelectionInVisibleArea:unused { + USE(unused); + HFController *controller = [self controller]; + NSArray *selection = [controller selectedContentsRanges]; + unsigned long long min = ULLONG_MAX, max = 0; + HFASSERT([selection count] >= 1); + FOREACH(HFRangeWrapper *, wrapper, selection) { + HFRange range = [wrapper HFRange]; + min = MIN(min, range.location); + max = MAX(max, HFMaxRange(range)); + } + HFASSERT(max >= min); + [controller maximizeVisibilityOfContentsRange:HFRangeMake(min, max - min)]; +} + +- (void)insertTab:unused { + USE(unused); + [[[self view] window] selectNextKeyView:nil]; +} + +- (void)insertBacktab:unused { + USE(unused); + [[[self view] window] selectPreviousKeyView:nil]; +} + +FORWARD(scrollLineUp) +FORWARD(scrollLineDown) +FORWARD(transpose) +FORWARD(transposeWords) + +FORWARD(selectParagraph) +FORWARD(selectLine) +FORWARD(selectWord) +FORWARD(indent) +//FORWARD(insertNewline) +FORWARD(insertParagraphSeparator) +FORWARD(insertNewlineIgnoringFieldEditor) +FORWARD(insertTabIgnoringFieldEditor) +FORWARD(insertLineBreak) +FORWARD(insertContainerBreak) +FORWARD(changeCaseOfLetter) +FORWARD(uppercaseWord) +FORWARD(lowercaseWord) +FORWARD(capitalizeWord) +FORWARD(deleteBackwardByDecomposingPreviousCharacter) +FORWARD(yank) +FORWARD(complete) +FORWARD(setMark) +FORWARD(deleteToMark) +FORWARD(selectToMark) +FORWARD(swapWithMark) +//FORWARD(cancelOperation) + +@end + diff --git a/bsnes/gb/HexFiend/HFTextVisualStyleRun.h b/bsnes/gb/HexFiend/HFTextVisualStyleRun.h new file mode 100644 index 00000000..0fa2ea74 --- /dev/null +++ b/bsnes/gb/HexFiend/HFTextVisualStyleRun.h @@ -0,0 +1,23 @@ +// +// HFTextVisualStyle.h +// HexFiend_2 +// +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import + +@interface HFTextVisualStyleRun : NSObject {} + +@property (nonatomic, copy) NSColor *foregroundColor; +@property (nonatomic, copy) NSColor *backgroundColor; +@property (nonatomic) NSRange range; +@property (nonatomic) BOOL shouldDraw; +@property (nonatomic) CGFloat scale; +@property (nonatomic, copy) NSIndexSet *bookmarkStarts; +@property (nonatomic, copy) NSIndexSet *bookmarkExtents; +@property (nonatomic, copy) NSIndexSet *bookmarkEnds; + +- (void)set; + +@end diff --git a/bsnes/gb/HexFiend/HFTextVisualStyleRun.m b/bsnes/gb/HexFiend/HFTextVisualStyleRun.m new file mode 100644 index 00000000..39442d53 --- /dev/null +++ b/bsnes/gb/HexFiend/HFTextVisualStyleRun.m @@ -0,0 +1,80 @@ +// +// HFTextVisualStyleRun.m +// HexFiend_2 +// +// Copyright 2009 ridiculous_fish. All rights reserved. +// + +#import "HFTextVisualStyleRun.h" + + +@implementation HFTextVisualStyleRun + +- (instancetype)init { + self = [super init]; + _scale = 1.; + _shouldDraw = YES; + return self; +} + +- (void)dealloc { + [_foregroundColor release]; + [_backgroundColor release]; + [_bookmarkStarts release]; + [_bookmarkExtents release]; + [_bookmarkEnds release]; + [super dealloc]; +} + +- (void)set { + [_foregroundColor set]; + if (_scale != (CGFloat)1.0) { + CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; + CGAffineTransform tm = CGContextGetTextMatrix(ctx); + /* Huge hack - adjust downward a little bit if we are scaling */ + tm = CGAffineTransformTranslate(tm, 0, -.25 * (_scale - 1)); + tm = CGAffineTransformScale(tm, _scale, _scale); + CGContextSetTextMatrix(ctx, tm); + } +} + +static inline NSUInteger flip(NSUInteger x) { + return _Generic(x, unsigned: NSSwapInt, unsigned long: NSSwapLong, unsigned long long: NSSwapLongLong)(x); +} +static inline NSUInteger rol(NSUInteger x, unsigned char r) { + r %= sizeof(NSUInteger)*8; + return (x << r) | (x << (sizeof(NSUInteger)*8 - r)); +} +- (NSUInteger)hash { + NSUInteger A = 0; + // All these hashes tend to have only low bits, except the double which has only high bits. +#define Q(x, r) rol(x, sizeof(NSUInteger)*r/6) + A ^= flip([_foregroundColor hash] ^ Q([_backgroundColor hash], 2)); // skew high + A ^= Q(_range.length ^ flip(_range.location), 2); // skew low + A ^= flip([_bookmarkStarts hash]) ^ Q([_bookmarkEnds hash], 3) ^ Q([_bookmarkExtents hash], 4); // skew high + A ^= _shouldDraw ? 0 : (NSUInteger)-1; + A ^= *(NSUInteger*)&_scale; // skew high + return A; +#undef Q +} + +- (BOOL)isEqual:(HFTextVisualStyleRun *)run { + if(![run isKindOfClass:[self class]]) return NO; + /* Check each field for equality. */ + if(!NSEqualRanges(_range, run->_range)) return NO; + if(_scale != run->_scale) return NO; + if(_shouldDraw != run->_shouldDraw) return NO; + if(!!_foregroundColor != !!run->_foregroundColor) return NO; + if(!!_backgroundColor != !!run->_backgroundColor) return NO; + if(!!_bookmarkStarts != !!run->_bookmarkStarts) return NO; + if(!!_bookmarkExtents != !!run->_bookmarkExtents) return NO; + if(!!_bookmarkEnds != !!run->_bookmarkEnds) return NO; + if(![_foregroundColor isEqual: run->_foregroundColor]) return NO; + if(![_backgroundColor isEqual: run->_backgroundColor]) return NO; + if(![_bookmarkStarts isEqual: run->_bookmarkStarts]) return NO; + if(![_bookmarkExtents isEqual: run->_bookmarkExtents]) return NO; + if(![_bookmarkEnds isEqual: run->_bookmarkEnds]) return NO; + return YES; +} + +@end diff --git a/bsnes/gb/HexFiend/HFTypes.h b/bsnes/gb/HexFiend/HFTypes.h new file mode 100644 index 00000000..b6ae6818 --- /dev/null +++ b/bsnes/gb/HexFiend/HFTypes.h @@ -0,0 +1,13 @@ +/*! @brief HFRange is the 64 bit analog of NSRange, containing a 64 bit location and length. */ +typedef struct { + unsigned long long location; + unsigned long long length; +} HFRange; + +/*! @brief HFFPRange is a struct used for representing floating point ranges, similar to NSRange. It contains two long doubles. + + This is useful for (for example) showing the range of visible lines. A double-precision value has 53 significant bits in the mantissa - so we would start to have precision problems at the high end of the range we can represent. Long double has a 64 bit mantissa on Intel, which means that we would start to run into trouble at the very very end of our range - barely acceptable. */ +typedef struct { + long double location; + long double length; +} HFFPRange; diff --git a/bsnes/gb/HexFiend/HFVerticalScrollerRepresenter.h b/bsnes/gb/HexFiend/HFVerticalScrollerRepresenter.h new file mode 100644 index 00000000..a14881ea --- /dev/null +++ b/bsnes/gb/HexFiend/HFVerticalScrollerRepresenter.h @@ -0,0 +1,21 @@ +// +// HFRepresenterVerticalScroller.h +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +#import + +/*! @class HFVerticalScrollerRepresenter + @brief An HFRepresenter responsible for showing a vertical scroll bar. + + HFVerticalScrollerRepresenter is an HFRepresenter whose view is a vertical NSScroller, that represents the current position within an HFController "document." It has no methods beyond those of HFRepresenter. + + As HFVerticalScrollerRepresenter is an especially simple representer, it makes for good sample code. +*/ +@interface HFVerticalScrollerRepresenter : HFRepresenter { + +} + +@end diff --git a/bsnes/gb/HexFiend/HFVerticalScrollerRepresenter.m b/bsnes/gb/HexFiend/HFVerticalScrollerRepresenter.m new file mode 100644 index 00000000..371b5687 --- /dev/null +++ b/bsnes/gb/HexFiend/HFVerticalScrollerRepresenter.m @@ -0,0 +1,133 @@ +// +// HFRepresenterVerticalScroller.m +// HexFiend_2 +// +// Copyright 2007 ridiculous_fish. All rights reserved. +// + +/* Note that on Tiger, NSScroller did not support double in any meaningful way; [scroller doubleValue] always returns 0, and setDoubleValue: doesn't look like it works either. */ + +#import + + +@implementation HFVerticalScrollerRepresenter + +/* No special NSCoding support needed */ + +- (NSView *)createView { + NSScroller *scroller = [[NSScroller alloc] initWithFrame:NSMakeRect(0, 0, [NSScroller scrollerWidthForControlSize:NSRegularControlSize scrollerStyle:NSScrollerStyleLegacy], 64)]; + [scroller setTarget:self]; + [scroller setContinuous:YES]; + [scroller setEnabled:YES]; + [scroller setTarget:self]; + [scroller setAction:@selector(scrollerDidChangeValue:)]; + [scroller setAutoresizingMask:NSViewHeightSizable]; + return scroller; +} + +- (NSUInteger)visibleLines { + HFController *controller = [self controller]; + HFASSERT(controller != NULL); + return ll2l(HFFPToUL(ceill([controller displayedLineRange].length))); +} + +- (void)scrollByKnobToValue:(double)newValue { + HFASSERT(newValue >= 0. && newValue <= 1.); + HFController *controller = [self controller]; + unsigned long long contentsLength = [controller contentsLength]; + NSUInteger bytesPerLine = [controller bytesPerLine]; + HFASSERT(bytesPerLine > 0); + unsigned long long totalLineCountTimesBytesPerLine = HFRoundUpToNextMultipleSaturate(contentsLength - 1, bytesPerLine); + HFASSERT(totalLineCountTimesBytesPerLine == ULLONG_MAX || totalLineCountTimesBytesPerLine % bytesPerLine == 0); + unsigned long long totalLineCount = HFDivideULLRoundingUp(totalLineCountTimesBytesPerLine, bytesPerLine); + HFFPRange currentLineRange = [controller displayedLineRange]; + HFASSERT(currentLineRange.length < HFULToFP(totalLineCount)); + long double maxScroll = totalLineCount - currentLineRange.length; + long double newScroll = maxScroll * (long double)newValue; + [controller setDisplayedLineRange:(HFFPRange){newScroll, currentLineRange.length}]; +} + +- (void)scrollByLines:(long long)linesInt { + if (linesInt == 0) return; + + //note - this properly computes the absolute value even for LLONG_MIN + long double lines = HFULToFP((unsigned long long)llabs(linesInt)); + + HFController *controller = [self controller]; + HFASSERT(controller != NULL); + HFFPRange displayedRange = [[self controller] displayedLineRange]; + if (linesInt < 0) { + displayedRange.location -= MIN(lines, displayedRange.location); + } + else { + long double availableLines = HFULToFP([controller totalLineCount]); + displayedRange.location = MIN(availableLines - displayedRange.length, displayedRange.location + lines); + } + [controller setDisplayedLineRange:displayedRange]; +} + +- (void)scrollerDidChangeValue:(NSScroller *)scroller { + assert(scroller == [self view]); + switch ([scroller hitPart]) { + case NSScrollerDecrementPage: [self scrollByLines: -(long long)[self visibleLines]]; break; + case NSScrollerIncrementPage: [self scrollByLines: (long long)[self visibleLines]]; break; + case NSScrollerDecrementLine: [self scrollByLines: -1LL]; break; + case NSScrollerIncrementLine: [self scrollByLines: 1LL]; break; + case NSScrollerKnob: [self scrollByKnobToValue:[scroller doubleValue]]; break; + default: break; + } +} + +- (void)updateScrollerValue { + HFController *controller = [self controller]; + CGFloat value, proportion; + NSScroller *scroller = [self view]; + BOOL enable = YES; + if (controller == nil) { + value = 0; + proportion = 0; + } + else { + unsigned long long length = [controller contentsLength]; + HFFPRange lineRange = [controller displayedLineRange]; + HFASSERT(lineRange.location >= 0 && lineRange.length >= 0); + if (length == 0) { + value = 0; + proportion = 1; + enable = NO; + } + else { + long double availableLines = HFULToFP([controller totalLineCount]); + long double consumedLines = MAX(1., lineRange.length); + proportion = ld2f(lineRange.length / availableLines); + + long double maxScroll = availableLines - consumedLines; + HFASSERT(maxScroll >= lineRange.location); + if (maxScroll == 0.) { + enable = NO; + value = 0; + } + else { + value = ld2f(lineRange.location / maxScroll); + } + } + } + [scroller setDoubleValue:value]; + [scroller setKnobProportion:proportion]; + [scroller setEnabled:enable]; +} + +- (CGFloat)minimumViewWidthForBytesPerLine:(NSUInteger)bytesPerLine { + USE(bytesPerLine); + return [NSScroller scrollerWidthForControlSize:[[self view] controlSize] scrollerStyle:NSScrollerStyleLegacy]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits { + if (bits & (HFControllerContentLength | HFControllerDisplayedLineRange)) [self updateScrollerValue]; +} + ++ (NSPoint)defaultLayoutPosition { + return NSMakePoint(2, 0); +} + +@end diff --git a/bsnes/gb/HexFiend/HexFiend.h b/bsnes/gb/HexFiend/HexFiend.h new file mode 100644 index 00000000..60d69a7e --- /dev/null +++ b/bsnes/gb/HexFiend/HexFiend.h @@ -0,0 +1,78 @@ +/*! @mainpage HexFiend.framework + * + * @section intro Introduction + * HexFiend.framework (hereafter "Hex Fiend" when there is no risk of confusion with the app by the same name) is a framework designed to enable applications to support viewing and editing of binary data. The emphasis is on editing data in a natural way, following Mac OS X text editing conventions. + * + * Hex Fiend is designed to work efficiently with large amounts (64 bits worth) of data. As such, it can work with arbitrarily large files without reading the entire file into memory. This includes insertions, deletions, and in-place editing. Hex Fiend can also efficiently save such changes back to the file, without requiring any additional temporary disk space. + * + * Hex Fiend has a clean separation between the model, view, and controller layers. The model layer allows for efficient manipulation of raw data of mixed sources, making it useful for tools that need to work with large files. + * + * Both the framework and the app are open source under a BSD-style license. In summary, you may use Hex Fiend in any project as long as you include the copyright notice somewhere in the documentation. + * + * @section requirements Requirements + * Hex Fiend is only available on Mac OS X, and supported on Mountain Lion and later. + * + * @section getting_started Getting Started + * + * The Hex Fiend source code is available at http://ridiculousfish.com/hexfiend/ and on GitHub at https://github.com/ridiculousfish/HexFiend + * + * Hex Fiend comes with some sample code ("HexFiendling"), distributed as part of the project. And of course the Hex Fiend application itself is open source, acting as a more sophisticated sample code. +*/ + + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + + +/* The following is all for Doxygen */ + + +/*! @defgroup model Model + * Hex Fiend's model classes + */ +///@{ +///@class HFByteArray +///@class HFBTreeByteArray +///@class HFFullMemoryByteArray +///@class HFByteSlice +///@class HFFileByteSlice +///@class HFSharedMemoryByteSlice +///@class HFFullMemoryByteSlice + +///@} + + +/*! @defgroup view View + * Hex Fiend's view classes + */ +///@{ +///@class HFRepresenter +///@class HFHexTextRepresenter +///@class HFStringEncodingTextRepresenter +///@class HFLayoutRepresenter +///@class HFLineCountingRepresenter +///@class HFStatusBarRepresenter +///@class HFVerticalScrollerRepresenter +///@class HFLineCountingRepresenter + +///@} + +/*! @defgroup controller Controller + * Hex Fiend's controller classes + */ +///@{ +///@class HFController + +///@} diff --git a/bsnes/gb/HexFiend/HexFiend_2_Framework_Prefix.pch b/bsnes/gb/HexFiend/HexFiend_2_Framework_Prefix.pch new file mode 100644 index 00000000..96d0fb76 --- /dev/null +++ b/bsnes/gb/HexFiend/HexFiend_2_Framework_Prefix.pch @@ -0,0 +1,99 @@ +// +// Prefix header for all source files of the 'HexFiend_2' target in the 'HexFiend_2' project +// + +#ifdef __OBJC__ + #import + #import +#endif + +#define PRIVATE_EXTERN __private_extern__ + +#include + +#if ! NDEBUG +#define HFASSERT(a) assert(a) +#else +#define HFASSERT(a) if (0 && ! (a)) abort() +#endif + + +#define UNIMPLEMENTED_VOID() [NSException raise:NSGenericException \ + format:@"Message %@ sent to instance of class %@, "\ + @"which does not implement that method",\ + NSStringFromSelector(_cmd), [[self class] description]] + +#define UNIMPLEMENTED() UNIMPLEMENTED_VOID(); return 0 + +/* Macro to "use" a variable to prevent unused variable warnings. */ +#define USE(x) ((void)(x)) + +#define check_malloc(x) ({ size_t _count = (x); void *_result = malloc(_count); if(!_result) { fprintf(stderr, "Out of memory allocating %lu bytes\n", (unsigned long)_count); exit(EXIT_FAILURE); } _result; }) +#define check_calloc(x) ({ size_t _count = (x); void *_result = calloc(_count, 1); if(!_result) { fprintf(stderr, "Out of memory allocating %lu bytes\n", (unsigned long)_count); exit(EXIT_FAILURE); } _result; }) +#define check_realloc(p, x) ({ size_t _count = (x); void *_result = realloc((p), x); if(!_result) { fprintf(stderr, "Out of memory reallocating %lu bytes\n", (unsigned long)_count); exit(EXIT_FAILURE); } _result; }) + +#if ! NDEBUG +#define REQUIRE_NOT_NULL(a) do { \ + if ((a)==NULL) {\ + fprintf(stderr, "REQUIRE_NOT_NULL failed: NULL value for parameter " #a " on line %d in file %s\n", __LINE__, __FILE__);\ + abort();\ + }\ +} while (0) + +#define EXPECT_CLASS(e, c) do { \ + if (! [(e) isKindOfClass:[c class]]) {\ + fprintf(stderr, "EXPECT_CLASS failed: Expression " #e " is %s on line %d in file %s\n", (e) ? "(nil)" : [[e description] UTF8String], __LINE__, __FILE__);\ + abort();\ + }\ +} while (0) + +#else +#define REQUIRE_NOT_NULL(a) USE(a) +#define EXPECT_CLASS(e, c) USE(e) +#endif + +#define FOREACH(type, var, exp) for (type var in (exp)) + +#define NEW_ARRAY(type, name, number) \ + type name ## static_ [256];\ + type * name = ((number) <= 256 ? name ## static_ : check_malloc((number) * sizeof(type))) + +#define FREE_ARRAY(name) \ + if (name != name ## static_) free(name) + +#if !defined(MIN) + #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#endif + +#if !defined(MAX) + #define MAX(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) +#endif + +//How many bytes should we read at a time when doing a find/replace? +#define SEARCH_CHUNK_SIZE 32768 + +//What's the smallest clipboard data size we should offer to avoid copying when quitting? This is 5 MB +#define MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT (5UL << 20) + +//What's the largest clipboard data size we should support exporting (at all?) This is 500 MB. Note that we can still copy more data than this internally, we just can't put it in, say, TextEdit. +#define MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT (500UL << 20) + +// When we save a file, and other byte arrays need to break their dependencies on the file by copying some of its data into memory, what's the max amount we should copy (per byte array)? We currently don't show any progress for this, so this should be a smaller value +#define MAX_MEMORY_TO_USE_FOR_BREAKING_FILE_DEPENDENCIES_ON_SAVE (16 * 1024 * 1024) + +#ifdef __OBJC__ + #import + #import "HFFunctions_Private.h" +#endif + +#ifndef __has_feature // Optional. +#define __has_feature(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef NS_RETURNS_RETAINED +#if __has_feature(attribute_ns_returns_retained) +#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained)) +#else +#define NS_RETURNS_RETAINED +#endif +#endif diff --git a/bsnes/gb/HexFiend/License.txt b/bsnes/gb/HexFiend/License.txt new file mode 100644 index 00000000..7760edbd --- /dev/null +++ b/bsnes/gb/HexFiend/License.txt @@ -0,0 +1,21 @@ +Copyright (c) 2005-2009, Peter Ammon +* All rights reserved. +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/bsnes/gb/JoyKit/ControllerConfiguration.inc b/bsnes/gb/JoyKit/ControllerConfiguration.inc new file mode 100644 index 00000000..ea3ba9a4 --- /dev/null +++ b/bsnes/gb/JoyKit/ControllerConfiguration.inc @@ -0,0 +1,477 @@ +#define BUTTON(x) @(JOYButtonUsageGeneric0 + (x)) +#define AXIS(x) @(JOYAxisUsageGeneric0 + (x)) +#define AXES2D(x) @(JOYAxes2DUsageGeneric0 + (x)) + +hacksByManufacturer = @{ + @(0x045E): @{ // Microsoft + /* Generally untested, but Microsoft goes by the book when it comes to HID report descriptors, so + it should work out of the box. The hack is only here for automatic mapping */ + + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(2), + @(kHIDUsage_GD_Rx): @(1), + @(kHIDUsage_GD_Ry): @(1), + @(kHIDUsage_GD_Rz): @(3), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageA), + BUTTON(2): @(JOYButtonUsageB), + BUTTON(3): @(JOYButtonUsageX), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(7): @(JOYButtonUsageLStick), + BUTTON(8): @(JOYButtonUsageRStick), + BUTTON(9): @(JOYButtonUsageStart), + BUTTON(10): @(JOYButtonUsageSelect), + BUTTON(11): @(JOYButtonUsageHome), + }, + + JOYAxisUsageMapping: @{ + AXIS(3): @(JOYAxisUsageL1), + AXIS(6): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + }, + + @(0x054C): @{ // Sony + /* Generally untested, but should work */ + + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(3), + @(kHIDUsage_GD_Rz): @(1), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageY), + BUTTON(2): @(JOYButtonUsageB), + BUTTON(3): @(JOYButtonUsageA), + BUTTON(4): @(JOYButtonUsageX), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(7): @(JOYButtonUsageL2), + BUTTON(8): @(JOYButtonUsageR2), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageHome), + BUTTON(14): @(JOYButtonUsageMisc), + }, + + JOYAxisUsageMapping: @{ + AXIS(4): @(JOYAxisUsageL1), + AXIS(5): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + } +}; + +hacksByName = @{ + @"WUP-028": @{ // Nintendo GameCube Controller Adapter + JOYReportIDFilters: @[@[@1], @[@2], @[@3], @[@4]], + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageA), + BUTTON(2): @(JOYButtonUsageB), + BUTTON(3): @(JOYButtonUsageX), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageStart), + BUTTON(6): @(JOYButtonUsageZ), + BUTTON(7): @(JOYButtonUsageR1), + BUTTON(8): @(JOYButtonUsageL1), + }, + + JOYAxisUsageMapping: @{ + AXIS(3): @(JOYAxisUsageL1), + AXIS(6): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(2), + @(kHIDUsage_GD_Rx): @(1), + @(kHIDUsage_GD_Ry): @(1), + @(kHIDUsage_GD_Rz): @(3), + }, + + JOYRumbleUsage: @1, + JOYRumbleUsagePage: @0xFF00, + + JOYConnectedUsage: @2, + JOYConnectedUsagePage: @0xFF00, + + JOYActivationReport: [NSData dataWithBytes:(uint8_t[]){0x13} length:1], + + JOYCustomReports: @{ + + // Rumble + @(-17): @[ + @{@"reportID": @(1), @"size":@1, @"offset":@0, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + @{@"reportID": @(2), @"size":@1, @"offset":@8, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + @{@"reportID": @(3), @"size":@1, @"offset":@16, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + @{@"reportID": @(4), @"size":@1, @"offset":@24, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1}, + ], + + @(33): @[ + + // Player 1 + + @{@"reportID": @(1), @"size":@1, @"offset":@4, @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + @{@"reportID": @(1), @"size":@1, @"offset":@8, @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(1), @"size":@1, @"offset":@9, @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(1), @"size":@1, @"offset":@10,@"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(1), @"size":@1, @"offset":@11,@"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(1), @"size":@1, @"offset":@12, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(1), @"size":@1, @"offset":@13, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(1), @"size":@1, @"offset":@14, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(1), @"size":@1, @"offset":@15, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(1), @"size":@1, @"offset":@16, @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(1), @"size":@1, @"offset":@17, @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(1), @"size":@1, @"offset":@18, @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(1), @"size":@1, @"offset":@19, @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(1), @"size":@8, @"offset":@24, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@32, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(1), @"size":@8, @"offset":@40, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@48, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(1), @"size":@8, @"offset":@56, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@64, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + // Player 2 + + @{@"reportID": @(2), @"size":@1, @"offset":@(4 + 72), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + + @{@"reportID": @(2), @"size":@1, @"offset":@(8 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(2), @"size":@1, @"offset":@(9 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(2), @"size":@1, @"offset":@(10 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(2), @"size":@1, @"offset":@(11 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(2), @"size":@1, @"offset":@(12 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(2), @"size":@1, @"offset":@(13 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(2), @"size":@1, @"offset":@(14 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(2), @"size":@1, @"offset":@(15 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(2), @"size":@1, @"offset":@(16 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(2), @"size":@1, @"offset":@(17 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(2), @"size":@1, @"offset":@(18 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(2), @"size":@1, @"offset":@(19 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(2), @"size":@8, @"offset":@(24 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(2), @"size":@8, @"offset":@(32 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(2), @"size":@8, @"offset":@(40 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(2), @"size":@8, @"offset":@(48 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(2), @"size":@8, @"offset":@(56 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(2), @"size":@8, @"offset":@(64 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + // Player 3 + + @{@"reportID": @(3), @"size":@1, @"offset":@(4 + 144), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + @{@"reportID": @(3), @"size":@1, @"offset":@(8 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(3), @"size":@1, @"offset":@(9 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(3), @"size":@1, @"offset":@(10 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(3), @"size":@1, @"offset":@(11 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(3), @"size":@1, @"offset":@(12 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(3), @"size":@1, @"offset":@(13 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(3), @"size":@1, @"offset":@(14 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(3), @"size":@1, @"offset":@(15 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(3), @"size":@1, @"offset":@(16 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(3), @"size":@1, @"offset":@(17 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(3), @"size":@1, @"offset":@(18 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(3), @"size":@1, @"offset":@(19 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(3), @"size":@8, @"offset":@(24 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(3), @"size":@8, @"offset":@(32 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(3), @"size":@8, @"offset":@(40 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(3), @"size":@8, @"offset":@(48 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(3), @"size":@8, @"offset":@(56 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(3), @"size":@8, @"offset":@(64 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + // Player 4 + + @{@"reportID": @(4), @"size":@1, @"offset":@(4 + 216), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1}, + + @{@"reportID": @(4), @"size":@1, @"offset":@(8 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(4), @"size":@1, @"offset":@(9 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(4), @"size":@1, @"offset":@(10 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(4), @"size":@1, @"offset":@(11 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@4}, + + @{@"reportID": @(4), @"size":@1, @"offset":@(12 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + @{@"reportID": @(4), @"size":@1, @"offset":@(13 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(4), @"size":@1, @"offset":@(14 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(4), @"size":@1, @"offset":@(15 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + + + @{@"reportID": @(4), @"size":@1, @"offset":@(16 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(4), @"size":@1, @"offset":@(17 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(4), @"size":@1, @"offset":@(18 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(4), @"size":@1, @"offset":@(19 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(4), @"size":@8, @"offset":@(24 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255}, + @{@"reportID": @(4), @"size":@8, @"offset":@(32 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0}, + + @{@"reportID": @(4), @"size":@8, @"offset":@(40 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(4), @"size":@8, @"offset":@(48 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0}, + + @{@"reportID": @(4), @"size":@8, @"offset":@(56 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255}, + @{@"reportID": @(4), @"size":@8, @"offset":@(64 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255}, + + + ]}, + }, + + @"GameCube Controller Adapter": @{ // GameCube Controller PC Adapter + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(3), + @(kHIDUsage_GD_Rz): @(1), + }, + JOYReportIDFilters: @[@[@1], @[@2], @[@3], @[@4]], + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageX), + BUTTON(2): @(JOYButtonUsageA), + BUTTON(3): @(JOYButtonUsageB), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(8): @(JOYButtonUsageZ), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(13): @(JOYButtonUsageDPadUp), + BUTTON(14): @(JOYButtonUsageDPadRight), + BUTTON(15): @(JOYButtonUsageDPadDown), + BUTTON(16): @(JOYButtonUsageDPadLeft), + }, + + JOYAxisUsageMapping: @{ + AXIS(4): @(JOYAxisUsageL1), + AXIS(5): @(JOYAxisUsageR1), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(3): @(JOYAxes2DUsageRightStick), + }, + + JOYRumbleUsage: @1, + JOYRumbleUsagePage: @0xFF00, + JOYRumbleMin: @0, + JOYRumbleMax: @255, + JOYSwapZRz: @YES, + }, + + @"Twin USB Joystick": @{ // DualShock PC Adapter + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(2), + @(kHIDUsage_GD_Rz): @(1), + }, + JOYReportIDFilters: @[@[@1], @[@2]], + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageX), + BUTTON(2): @(JOYButtonUsageA), + BUTTON(3): @(JOYButtonUsageB), + BUTTON(4): @(JOYButtonUsageY), + BUTTON(5): @(JOYButtonUsageL2), + BUTTON(6): @(JOYButtonUsageR2), + BUTTON(7): @(JOYButtonUsageL1), + BUTTON(8): @(JOYButtonUsageR1), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageDPadUp), + BUTTON(14): @(JOYButtonUsageDPadRight), + BUTTON(15): @(JOYButtonUsageDPadDown), + BUTTON(16): @(JOYButtonUsageDPadLeft), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(6): @(JOYAxes2DUsageRightStick), + }, + + JOYSwapZRz: @YES, + }, + + @"Pro Controller": @{ // Switch Pro Controller + JOYIsSwitch: @YES, + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(0), + @(kHIDUsage_GD_Rx): @(1), + @(kHIDUsage_GD_Ry): @(1), + @(kHIDUsage_GD_Rz): @(1), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageB), + BUTTON(2): @(JOYButtonUsageA), + BUTTON(3): @(JOYButtonUsageY), + BUTTON(4): @(JOYButtonUsageX), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(7): @(JOYButtonUsageL2), + BUTTON(8): @(JOYButtonUsageR2), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageHome), + BUTTON(14): @(JOYButtonUsageMisc), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + + JOYCustomReports: @{ + @(0x30): @[ + + // For USB mode, which uses the wrong report descriptor + + @{@"reportID": @(1), @"size":@1, @"offset":@16, @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(1), @"size":@1, @"offset":@17, @"usagePage":@(kHIDPage_Button), @"usage":@4}, + @{@"reportID": @(1), @"size":@1, @"offset":@18, @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(1), @"size":@1, @"offset":@19, @"usagePage":@(kHIDPage_Button), @"usage":@2}, + + // SR and SL not used on the Pro Controller + @{@"reportID": @(1), @"size":@1, @"offset":@22, @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(1), @"size":@1, @"offset":@23, @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(1), @"size":@1, @"offset":@24, @"usagePage":@(kHIDPage_Button), @"usage":@9}, + @{@"reportID": @(1), @"size":@1, @"offset":@25, @"usagePage":@(kHIDPage_Button), @"usage":@10}, + @{@"reportID": @(1), @"size":@1, @"offset":@26, @"usagePage":@(kHIDPage_Button), @"usage":@12}, + @{@"reportID": @(1), @"size":@1, @"offset":@27, @"usagePage":@(kHIDPage_Button), @"usage":@11}, + + @{@"reportID": @(1), @"size":@1, @"offset":@28, @"usagePage":@(kHIDPage_Button), @"usage":@13}, + @{@"reportID": @(1), @"size":@1, @"offset":@29, @"usagePage":@(kHIDPage_Button), @"usage":@14}, + + @{@"reportID": @(1), @"size":@1, @"offset":@32, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(1), @"size":@1, @"offset":@33, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + @{@"reportID": @(1), @"size":@1, @"offset":@34, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(1), @"size":@1, @"offset":@35, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + + // SR and SL not used on the Pro Controller + @{@"reportID": @(1), @"size":@1, @"offset":@38, @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(1), @"size":@1, @"offset":@39, @"usagePage":@(kHIDPage_Button), @"usage":@7}, + + /* Sticks */ + @{@"reportID": @(1), @"size":@12, @"offset":@40, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @0xFFF}, + @{@"reportID": @(1), @"size":@12, @"offset":@52, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @0xFFF, @"max": @0}, + + @{@"reportID": @(1), @"size":@12, @"offset":@64, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @0xFFF}, + @{@"reportID": @(1), @"size":@12, @"offset":@76, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0xFFF, @"max": @0}, + ], + }, + }, + + JOYIgnoredReports: @(0x30), // Ignore the real 0x30 report as it's broken + + @"PLAYSTATION(R)3 Controller": @{ // DualShock 3 + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(3), + @(kHIDUsage_GD_Rz): @(1), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageSelect), + BUTTON(2): @(JOYButtonUsageL3), + BUTTON(3): @(JOYButtonUsageR3), + BUTTON(4): @(JOYButtonUsageStart), + BUTTON(5): @(JOYButtonUsageDPadUp), + BUTTON(6): @(JOYButtonUsageDPadRight), + BUTTON(7): @(JOYButtonUsageDPadDown), + BUTTON(8): @(JOYButtonUsageDPadLeft), + BUTTON(9): @(JOYButtonUsageL2), + BUTTON(10): @(JOYButtonUsageR2), + BUTTON(11): @(JOYButtonUsageL1), + BUTTON(12): @(JOYButtonUsageR1), + BUTTON(13): @(JOYButtonUsageX), + BUTTON(14): @(JOYButtonUsageA), + BUTTON(15): @(JOYButtonUsageB), + BUTTON(16): @(JOYButtonUsageY), + BUTTON(17): @(JOYButtonUsageHome), + }, + + JOYAxisUsageMapping: @{ + AXIS(4): @(JOYAxisUsageL1), + AXIS(5): @(JOYAxisUsageR1), + AXIS(8): @(JOYAxisUsageL2), + AXIS(9): @(JOYAxisUsageR2), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + AXES2D(3): @(JOYAxes2DUsageRightStick), + }, + + JOYCustomReports: @{ + @(0x01): @[ + /* Pressure sensitive inputs */ + @{@"reportID": @(1), @"size":@8, @"offset":@(13 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(14 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(15 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(16 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(17 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Dial), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(18 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Wheel), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(19 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(20 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(21 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(22 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(23 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + @{@"reportID": @(1), @"size":@8, @"offset":@(24 * 8), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Slider), @"min": @0, @"max": @255}, + ] + }, + + JOYIsDualShock3: @YES, + }, + +}; diff --git a/bsnes/gb/JoyKit/JOYAxes2D.h b/bsnes/gb/JoyKit/JOYAxes2D.h new file mode 100644 index 00000000..b6f6d152 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYAxes2D.h @@ -0,0 +1,24 @@ +#import + +typedef enum { + JOYAxes2DUsageNone, + JOYAxes2DUsageLeftStick, + JOYAxes2DUsageRightStick, + JOYAxes2DUsageMiddleStick, + JOYAxes2DUsagePointer, + JOYAxes2DUsageNonGenericMax, + + JOYAxes2DUsageGeneric0 = 0x10000, +} JOYAxes2DUsage; + +@interface JOYAxes2D : NSObject +- (NSString *)usageString; ++ (NSString *)usageToString: (JOYAxes2DUsage) usage; +- (uint64_t)uniqueID; +- (double)distance; +- (double)angle; +- (NSPoint)value; +@property JOYAxes2DUsage usage; +@end + + diff --git a/bsnes/gb/JoyKit/JOYAxes2D.m b/bsnes/gb/JoyKit/JOYAxes2D.m new file mode 100644 index 00000000..272d34f9 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYAxes2D.m @@ -0,0 +1,181 @@ +#import "JOYAxes2D.h" +#import "JOYElement.h" + +@implementation JOYAxes2D +{ + JOYElement *_element1, *_element2; + double _state1, _state2; + int32_t initialX, initialY; + int32_t minX, minY; + int32_t maxX, maxY; + +} + ++ (NSString *)usageToString: (JOYAxes2DUsage) usage +{ + if (usage < JOYAxes2DUsageNonGenericMax) { + return (NSString *[]) { + @"None", + @"Left Stick", + @"Right Stick", + @"Middle Stick", + @"Pointer", + }[usage]; + } + if (usage >= JOYAxes2DUsageGeneric0) { + return [NSString stringWithFormat:@"Generic 2D Analog Control %d", usage - JOYAxes2DUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage 2D Axes %d", usage]; +} + +- (NSString *)usageString +{ + return [self.class usageToString:_usage]; +} + +- (uint64_t)uniqueID +{ + return _element1.uniqueID; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %.2f%%, %.2f degrees>", self.className, self, self.usageString, self.uniqueID, self.distance * 100, self.angle]; +} + +- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2 +{ + self = [super init]; + if (!self) return self; + + _element1 = element1; + _element2 = element2; + + + if (element1.usagePage == kHIDPage_GenericDesktop) { + uint16_t usage = element1.usage; + _usage = JOYAxes2DUsageGeneric0 + usage - kHIDUsage_GD_X + 1; + } + initialX = 0; + initialY = 0; + minX = element1.max; + minY = element2.max; + maxX = element1.min; + maxY = element2.min; + + return self; +} + +- (NSPoint)value +{ + return NSMakePoint(_state1, _state2); +} + +-(int32_t) effectiveMinX +{ + int32_t rawMin = _element1.min; + int32_t rawMax = _element1.max; + if (initialX == 0) return rawMin; + if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return minX; + if ((initialX - rawMin) < (rawMax - initialX)) return rawMin; + return initialX - (rawMax - initialX); +} + +-(int32_t) effectiveMinY +{ + int32_t rawMin = _element2.min; + int32_t rawMax = _element2.max; + if (initialY == 0) return rawMin; + if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return minY; + if ((initialY - rawMin) < (rawMax - initialY)) return rawMin; + return initialY - (rawMax - initialY); +} + +-(int32_t) effectiveMaxX +{ + int32_t rawMin = _element1.min; + int32_t rawMax = _element1.max; + if (initialX == 0) return rawMax; + if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return maxX; + if ((initialX - rawMin) > (rawMax - initialX)) return rawMax; + return initialX + (initialX - rawMin); +} + +-(int32_t) effectiveMaxY +{ + int32_t rawMin = _element2.min; + int32_t rawMax = _element2.max; + if (initialY == 0) return rawMax; + if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return maxY; + if ((initialY - rawMin) > (rawMax - initialY)) return rawMax; + return initialY + (initialY - rawMin); +} + +- (bool)updateState +{ + int32_t x = [_element1 value]; + int32_t y = [_element2 value]; + if (x == 0 && y == 0) return false; + + if (initialX == 0 && initialY == 0) { + initialX = x; + initialY = y; + } + + double old1 = _state1, old2 = _state2; + { + int32_t value = x; + + if (initialX != 0) { + minX = MIN(value, minX); + maxX = MAX(value, maxX); + } + + double min = [self effectiveMinX]; + double max = [self effectiveMaxX]; + if (min == max) return false; + + _state1 = (value - min) / (max - min) * 2 - 1; + } + + { + int32_t value = y; + + if (initialY != 0) { + minY = MIN(value, minY); + maxY = MAX(value, maxY); + } + + double min = [self effectiveMinY]; + double max = [self effectiveMaxY]; + if (min == max) return false; + + _state2 = (value - min) / (max - min) * 2 - 1; + } + + if (_state1 < -1 || _state1 > 1 || + _state2 < -1 || _state2 > 1) { + // Makes no sense, recalibrate + _state1 = _state2 = 0; + initialX = initialY = 0; + minX = _element1.max; + minY = _element2.max; + maxX = _element1.min; + maxY = _element2.min; + } + + return old1 != _state1 || old2 != _state2; +} + +- (double)distance +{ + return MIN(sqrt(_state1 * _state1 + _state2 * _state2), 1.0); +} + +- (double)angle { + double temp = atan2(_state2, _state1) * 180 / M_PI; + if (temp >= 0) return temp; + return temp + 360; +} +@end diff --git a/bsnes/gb/JoyKit/JOYAxis.h b/bsnes/gb/JoyKit/JOYAxis.h new file mode 100644 index 00000000..5a4c1669 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYAxis.h @@ -0,0 +1,29 @@ +#import + +typedef enum { + JOYAxisUsageNone, + JOYAxisUsageL1, + JOYAxisUsageL2, + JOYAxisUsageL3, + JOYAxisUsageR1, + JOYAxisUsageR2, + JOYAxisUsageR3, + JOYAxisUsageWheel, + JOYAxisUsageRudder, + JOYAxisUsageThrottle, + JOYAxisUsageAccelerator, + JOYAxisUsageBrake, + JOYAxisUsageNonGenericMax, + + JOYAxisUsageGeneric0 = 0x10000, +} JOYAxisUsage; + +@interface JOYAxis : NSObject +- (NSString *)usageString; ++ (NSString *)usageToString: (JOYAxisUsage) usage; +- (uint64_t)uniqueID; +- (double)value; +@property JOYAxisUsage usage; +@end + + diff --git a/bsnes/gb/JoyKit/JOYAxis.m b/bsnes/gb/JoyKit/JOYAxis.m new file mode 100644 index 00000000..169eaee8 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYAxis.m @@ -0,0 +1,90 @@ +#import "JOYAxis.h" +#import "JOYElement.h" + +@implementation JOYAxis +{ + JOYElement *_element; + double _state; + double _min; +} + ++ (NSString *)usageToString: (JOYAxisUsage) usage +{ + if (usage < JOYAxisUsageNonGenericMax) { + return (NSString *[]) { + @"None", + @"Analog L1", + @"Analog L2", + @"Analog L3", + @"Analog R1", + @"Analog R2", + @"Analog R3", + @"Wheel", + @"Rudder", + @"Throttle", + @"Accelerator", + @"Brake", + }[usage]; + } + if (usage >= JOYAxisUsageGeneric0) { + return [NSString stringWithFormat:@"Generic Analog Control %d", usage - JOYAxisUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage Axis %d", usage]; +} + +- (NSString *)usageString +{ + return [self.class usageToString:_usage]; +} + +- (uint64_t)uniqueID +{ + return _element.uniqueID; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %f%%>", self.className, self, self.usageString, self.uniqueID, _state * 100]; +} + +- (instancetype)initWithElement:(JOYElement *)element +{ + self = [super init]; + if (!self) return self; + + _element = element; + + + if (element.usagePage == kHIDPage_GenericDesktop) { + uint16_t usage = element.usage; + _usage = JOYAxisUsageGeneric0 + usage - kHIDUsage_GD_X + 1; + } + + _min = 1.0; + + return self; +} + +- (double) value +{ + return _state; +} + +- (bool)updateState +{ + double min = _element.min; + double max = _element.max; + if (min == max) return false; + double old = _state; + double unnormalized = ([_element value] - min) / (max - min); + if (unnormalized < _min) { + _min = unnormalized; + } + if (_min != 1) { + _state = (unnormalized - _min) / (1 - _min); + } + return old != _state; +} + +@end diff --git a/bsnes/gb/JoyKit/JOYButton.h b/bsnes/gb/JoyKit/JOYButton.h new file mode 100644 index 00000000..f732c8e6 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYButton.h @@ -0,0 +1,42 @@ +#import + + + +typedef enum { + JOYButtonUsageNone, + JOYButtonUsageA, + JOYButtonUsageB, + JOYButtonUsageC, + JOYButtonUsageX, + JOYButtonUsageY, + JOYButtonUsageZ, + JOYButtonUsageStart, + JOYButtonUsageSelect, + JOYButtonUsageHome, + JOYButtonUsageMisc, + JOYButtonUsageLStick, + JOYButtonUsageRStick, + JOYButtonUsageL1, + JOYButtonUsageL2, + JOYButtonUsageL3, + JOYButtonUsageR1, + JOYButtonUsageR2, + JOYButtonUsageR3, + JOYButtonUsageDPadLeft, + JOYButtonUsageDPadRight, + JOYButtonUsageDPadUp, + JOYButtonUsageDPadDown, + JOYButtonUsageNonGenericMax, + + JOYButtonUsageGeneric0 = 0x10000, +} JOYButtonUsage; + +@interface JOYButton : NSObject +- (NSString *)usageString; ++ (NSString *)usageToString: (JOYButtonUsage) usage; +- (uint64_t)uniqueID; +- (bool) isPressed; +@property JOYButtonUsage usage; +@end + + diff --git a/bsnes/gb/JoyKit/JOYButton.m b/bsnes/gb/JoyKit/JOYButton.m new file mode 100644 index 00000000..3e6026d1 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYButton.m @@ -0,0 +1,102 @@ +#import "JOYButton.h" +#import "JOYElement.h" + +@implementation JOYButton +{ + JOYElement *_element; + bool _state; +} + ++ (NSString *)usageToString: (JOYButtonUsage) usage +{ + if (usage < JOYButtonUsageNonGenericMax) { + return (NSString *[]) { + @"None", + @"A", + @"B", + @"C", + @"X", + @"Y", + @"Z", + @"Start", + @"Select", + @"Home", + @"Misc", + @"Left Stick", + @"Right Stick", + @"L1", + @"L2", + @"L3", + @"R1", + @"R2", + @"R3", + @"D-Pad Left", + @"D-Pad Right", + @"D-Pad Up", + @"D-Pad Down", + }[usage]; + } + if (usage >= JOYButtonUsageGeneric0) { + return [NSString stringWithFormat:@"Generic Button %d", usage - JOYButtonUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage Button %d", usage]; +} + +- (NSString *)usageString +{ + return [self.class usageToString:_usage]; +} + +- (uint64_t)uniqueID +{ + return _element.uniqueID; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %s>", self.className, self, self.usageString, self.uniqueID, _state? "Presssed" : "Released"]; +} + +- (instancetype)initWithElement:(JOYElement *)element +{ + self = [super init]; + if (!self) return self; + + _element = element; + + if (element.usagePage == kHIDPage_Button) { + uint16_t usage = element.usage; + _usage = JOYButtonUsageGeneric0 + usage; + } + else if (element.usagePage == kHIDPage_GenericDesktop) { + switch (element.usage) { + case kHIDUsage_GD_DPadUp: _usage = JOYButtonUsageDPadUp; break; + case kHIDUsage_GD_DPadDown: _usage = JOYButtonUsageDPadDown; break; + case kHIDUsage_GD_DPadRight: _usage = JOYButtonUsageDPadRight; break; + case kHIDUsage_GD_DPadLeft: _usage = JOYButtonUsageDPadLeft; break; + case kHIDUsage_GD_Start: _usage = JOYButtonUsageStart; break; + case kHIDUsage_GD_Select: _usage = JOYButtonUsageSelect; break; + case kHIDUsage_GD_SystemMainMenu: _usage = JOYButtonUsageHome; break; + } + } + + return self; +} + +- (bool) isPressed +{ + return _state; +} + +- (bool)updateState +{ + bool state = [_element value]; + if (_state != state) { + _state = state; + return true; + } + return false; +} + +@end diff --git a/bsnes/gb/JoyKit/JOYController.h b/bsnes/gb/JoyKit/JOYController.h new file mode 100644 index 00000000..9ed7cf7b --- /dev/null +++ b/bsnes/gb/JoyKit/JOYController.h @@ -0,0 +1,41 @@ +#import +#import "JOYButton.h" +#import "JOYAxis.h" +#import "JOYAxes2D.h" +#import "JOYHat.h" + +static NSString const *JOYAxesEmulateButtonsKey = @"JOYAxesEmulateButtons"; +static NSString const *JOYAxes2DEmulateButtonsKey = @"JOYAxes2DEmulateButtons"; +static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; + +@class JOYController; + +@protocol JOYListener + +@optional +-(void) controllerConnected:(JOYController *)controller; +-(void) controllerDisconnected:(JOYController *)controller; +-(void) controller:(JOYController *)controller buttonChangedState:(JOYButton *)button; +-(void) controller:(JOYController *)controller movedAxis:(JOYAxis *)axis; +-(void) controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes; +-(void) controller:(JOYController *)controller movedHat:(JOYHat *)hat; + +@end + +@interface JOYController : NSObject ++ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options; ++ (NSArray *) allControllers; ++ (void) registerListener:(id)listener; ++ (void) unregisterListener:(id)listener; +- (NSString *)deviceName; +- (NSString *)uniqueID; +- (NSArray *) buttons; +- (NSArray *) axes; +- (NSArray *) axes2D; +- (NSArray *) hats; +- (void)setRumbleAmplitude:(double)amp; +- (void)setPlayerLEDs:(uint8_t)mask; +@property (readonly, getter=isConnected) bool connected; +@end + + diff --git a/bsnes/gb/JoyKit/JOYController.m b/bsnes/gb/JoyKit/JOYController.m new file mode 100644 index 00000000..ca2d1b18 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYController.m @@ -0,0 +1,913 @@ +#import "JOYController.h" +#import "JOYMultiplayerController.h" +#import "JOYElement.h" +#import "JOYSubElement.h" +#import "JOYFullReportElement.h" + +#import "JOYEmulatedButton.h" +#include + +#define PWM_RESOLUTION 16 + +static NSString const *JOYAxisGroups = @"JOYAxisGroups"; +static NSString const *JOYReportIDFilters = @"JOYReportIDFilters"; +static NSString const *JOYButtonUsageMapping = @"JOYButtonUsageMapping"; +static NSString const *JOYAxisUsageMapping = @"JOYAxisUsageMapping"; +static NSString const *JOYAxes2DUsageMapping = @"JOYAxes2DUsageMapping"; +static NSString const *JOYCustomReports = @"JOYCustomReports"; +static NSString const *JOYIsSwitch = @"JOYIsSwitch"; +static NSString const *JOYRumbleUsage = @"JOYRumbleUsage"; +static NSString const *JOYRumbleUsagePage = @"JOYRumbleUsagePage"; +static NSString const *JOYConnectedUsage = @"JOYConnectedUsage"; +static NSString const *JOYConnectedUsagePage = @"JOYConnectedUsagePage"; +static NSString const *JOYRumbleMin = @"JOYRumbleMin"; +static NSString const *JOYRumbleMax = @"JOYRumbleMax"; +static NSString const *JOYSwapZRz = @"JOYSwapZRz"; +static NSString const *JOYActivationReport = @"JOYActivationReport"; +static NSString const *JOYIgnoredReports = @"JOYIgnoredReports"; +static NSString const *JOYIsDualShock3 = @"JOYIsDualShock3"; + +static NSMutableDictionary *controllers; // Physical controllers +static NSMutableArray *exposedControllers; // Logical controllers + +static NSDictionary *hacksByName = nil; +static NSDictionary *hacksByManufacturer = nil; + +static NSMutableSet> *listeners = nil; + +static bool axesEmulateButtons = false; +static bool axes2DEmulateButtons = false; +static bool hatsEmulateButtons = false; + +@interface JOYController () ++ (void)controllerAdded:(IOHIDDeviceRef) device; ++ (void)controllerRemoved:(IOHIDDeviceRef) device; +- (void)elementChanged:(IOHIDElementRef) element; +- (void)gotReport:(NSData *)report; + +@end + +@interface JOYButton () +- (instancetype)initWithElement:(JOYElement *)element; +- (bool)updateState; +@end + +@interface JOYAxis () +- (instancetype)initWithElement:(JOYElement *)element; +- (bool)updateState; +@end + +@interface JOYHat () +- (instancetype)initWithElement:(JOYElement *)element; +- (bool)updateState; +@end + +@interface JOYAxes2D () +- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2; +- (bool)updateState; +@end + +static NSDictionary *CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage) +{ + return @{ + @kIOHIDDeviceUsagePageKey: @(page), + @kIOHIDDeviceUsageKey: @(usage), + }; +} + +static void HIDDeviceAdded(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + [JOYController controllerAdded:device]; +} + +static void HIDDeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) +{ + [JOYController controllerRemoved:device]; +} + +static void HIDInput(void *context, IOReturn result, void *sender, IOHIDValueRef value) +{ + [(__bridge JOYController *)context elementChanged:IOHIDValueGetElement(value)]; +} + +static void HIDReport(void *context, IOReturn result, void *sender, IOHIDReportType type, + uint32_t reportID, uint8_t *report, CFIndex reportLength) +{ + if (reportLength) { + [(__bridge JOYController *)context gotReport:[[NSData alloc] initWithBytesNoCopy:report length:reportLength freeWhenDone:NO]]; + } +} + +typedef struct __attribute__((packed)) { + uint8_t reportID; + uint8_t sequence; + uint8_t rumbleData[8]; + uint8_t command; + uint8_t commandData[26]; +} JOYSwitchPacket; + +typedef struct __attribute__((packed)) { + uint8_t reportID; + uint8_t padding; + uint8_t rumbleRightDuration; + uint8_t rumbleRightStrength; + uint8_t rumbleLeftDuration; + uint8_t rumbleLeftStrength; + uint32_t padding2; + uint8_t ledsEnabled; + struct { + uint8_t timeEnabled; + uint8_t dutyLength; + uint8_t enabled; + uint8_t dutyOff; + uint8_t dutyOn; + } __attribute__((packed)) led[5]; + uint8_t padding3[13]; +} JOYDualShock3Output; + +typedef union { + JOYSwitchPacket switchPacket; + JOYDualShock3Output ds3Output; +} JOYVendorSpecificOutput; + +@implementation JOYController +{ + IOHIDDeviceRef _device; + NSMutableDictionary *_buttons; + NSMutableDictionary *_axes; + NSMutableDictionary *_axes2D; + NSMutableDictionary *_hats; + NSMutableDictionary *_fullReportElements; + NSMutableDictionary *> *_multiElements; + + // Button emulation + NSMutableDictionary *_axisEmulatedButtons; + NSMutableDictionary *> *_axes2DEmulatedButtons; + NSMutableDictionary *> *_hatEmulatedButtons; + + JOYElement *_rumbleElement; + JOYElement *_connectedElement; + NSMutableDictionary *_iokitToJOY; + NSString *_serialSuffix; + bool _isSwitch; // Does this controller use the Switch protocol? + bool _isDualShock3; // Does this controller use DS3 outputs? + JOYVendorSpecificOutput _lastVendorSpecificOutput; + volatile double _rumbleAmplitude; + bool _physicallyConnected; + bool _logicallyConnected; + + NSDictionary *_hacks; + NSMutableData *_lastReport; + + // Used when creating inputs + JOYElement *_previousAxisElement; + + uint8_t _playerLEDs; + double _sentRumbleAmp; + unsigned _rumbleCounter; + bool _deviceCantSendReports; +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks +{ + return [self initWithDevice:device reportIDFilter:nil serialSuffix:nil hacks:hacks]; +} + +-(void)createOutputForElement:(JOYElement *)element +{ + uint16_t rumbleUsagePage = (uint16_t)[_hacks[JOYRumbleUsagePage] unsignedIntValue]; + uint16_t rumbleUsage = (uint16_t)[_hacks[JOYRumbleUsage] unsignedIntValue]; + + if (!_rumbleElement && rumbleUsage && rumbleUsagePage && element.usage == rumbleUsage && element.usagePage == rumbleUsagePage) { + if (_hacks[JOYRumbleMin]) { + element.min = [_hacks[JOYRumbleMin] unsignedIntValue]; + } + if (_hacks[JOYRumbleMax]) { + element.max = [_hacks[JOYRumbleMax] unsignedIntValue]; + } + _rumbleElement = element; + } +} + +-(void)createInputForElement:(JOYElement *)element +{ + uint16_t connectedUsagePage = (uint16_t)[_hacks[JOYConnectedUsagePage] unsignedIntValue]; + uint16_t connectedUsage = (uint16_t)[_hacks[JOYConnectedUsage] unsignedIntValue]; + + if (!_connectedElement && connectedUsage && connectedUsagePage && element.usage == connectedUsage && element.usagePage == connectedUsagePage) { + _connectedElement = element; + _logicallyConnected = element.value != element.min; + return; + } + + if (element.usagePage == kHIDPage_Button) { + button: { + JOYButton *button = [[JOYButton alloc] initWithElement: element]; + [_buttons setObject:button forKey:element]; + NSNumber *replacementUsage = _hacks[JOYButtonUsageMapping][@(button.usage)]; + if (replacementUsage) { + button.usage = [replacementUsage unsignedIntValue]; + } + return; + } + } + else if (element.usagePage == kHIDPage_GenericDesktop) { + NSDictionary *axisGroups = @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(1), + @(kHIDUsage_GD_Rx): @(2), + @(kHIDUsage_GD_Ry): @(2), + @(kHIDUsage_GD_Rz): @(1), + }; + + axisGroups = _hacks[JOYAxisGroups] ?: axisGroups; + + switch (element.usage) { + case kHIDUsage_GD_X: + case kHIDUsage_GD_Y: + case kHIDUsage_GD_Z: + case kHIDUsage_GD_Rx: + case kHIDUsage_GD_Ry: + case kHIDUsage_GD_Rz: { + + JOYElement *other = _previousAxisElement; + _previousAxisElement = element; + if (!other) goto single; + if (other.usage >= element.usage) goto single; + if (other.reportID != element.reportID) goto single; + if (![axisGroups[@(other.usage)] isEqualTo: axisGroups[@(element.usage)]]) goto single; + if (other.parentID != element.parentID) goto single; + + JOYAxes2D *axes = nil; + if (other.usage == kHIDUsage_GD_Z && element.usage == kHIDUsage_GD_Rz && [_hacks[JOYSwapZRz] boolValue]) { + axes = [[JOYAxes2D alloc] initWithFirstElement:element secondElement:other]; + } + else { + axes = [[JOYAxes2D alloc] initWithFirstElement:other secondElement:element]; + } + NSNumber *replacementUsage = _hacks[JOYAxes2DUsageMapping][@(axes.usage)]; + if (replacementUsage) { + axes.usage = [replacementUsage unsignedIntValue]; + } + + [_axisEmulatedButtons removeObjectForKey:@(_axes[other].uniqueID)]; + [_axes removeObjectForKey:other]; + _previousAxisElement = nil; + _axes2D[other] = axes; + _axes2D[element] = axes; + + if (axes2DEmulateButtons) { + _axes2DEmulatedButtons[@(axes.uniqueID)] = @[ + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:axes.uniqueID | 0x100000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:axes.uniqueID | 0x200000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:axes.uniqueID | 0x300000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:axes.uniqueID | 0x400000000L], + ]; + } + + /* + for (NSArray *group in axes2d) { + break; + IOHIDElementRef first = (__bridge IOHIDElementRef)group[0]; + IOHIDElementRef second = (__bridge IOHIDElementRef)group[1]; + if (IOHIDElementGetUsage(first) > element.usage) continue; + if (IOHIDElementGetUsage(second) > element.usage) continue; + if (IOHIDElementGetReportID(first) != IOHIDElementGetReportID(element)) continue; + if ((IOHIDElementGetUsage(first) - kHIDUsage_GD_X) / 3 != (element.usage - kHIDUsage_GD_X) / 3) continue; + if (IOHIDElementGetParent(first) != IOHIDElementGetParent(element)) continue; + + [axes2d removeObject:group]; + [axes3d addObject:@[(__bridge id)first, (__bridge id)second, _element]]; + found = true; + break; + }*/ + break; + } + single: + case kHIDUsage_GD_Slider: + case kHIDUsage_GD_Dial: + case kHIDUsage_GD_Wheel: { + JOYAxis *axis = [[JOYAxis alloc] initWithElement: element]; + [_axes setObject:axis forKey:element]; + + NSNumber *replacementUsage = _hacks[JOYAxisUsageMapping][@(axis.usage)]; + if (replacementUsage) { + axis.usage = [replacementUsage unsignedIntValue]; + } + + if (axesEmulateButtons && axis.usage >= JOYAxisUsageL1 && axis.usage <= JOYAxisUsageR3) { + _axisEmulatedButtons[@(axis.uniqueID)] = + [[JOYEmulatedButton alloc] initWithUsage:axis.usage - JOYAxisUsageL1 + JOYButtonUsageL1 uniqueID:axis.uniqueID]; + } + + if (axesEmulateButtons && axis.usage >= JOYAxisUsageGeneric0) { + _axisEmulatedButtons[@(axis.uniqueID)] = + [[JOYEmulatedButton alloc] initWithUsage:axis.usage - JOYAxisUsageGeneric0 + JOYButtonUsageGeneric0 uniqueID:axis.uniqueID]; + } + + break; + } + case kHIDUsage_GD_DPadUp: + case kHIDUsage_GD_DPadDown: + case kHIDUsage_GD_DPadRight: + case kHIDUsage_GD_DPadLeft: + case kHIDUsage_GD_Start: + case kHIDUsage_GD_Select: + case kHIDUsage_GD_SystemMainMenu: + goto button; + + case kHIDUsage_GD_Hatswitch: { + JOYHat *hat = [[JOYHat alloc] initWithElement: element]; + [_hats setObject:hat forKey:element]; + if (hatsEmulateButtons) { + _hatEmulatedButtons[@(hat.uniqueID)] = @[ + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:hat.uniqueID | 0x100000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:hat.uniqueID | 0x200000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:hat.uniqueID | 0x300000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:hat.uniqueID | 0x400000000L], + ]; + } + break; + } + } + } +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef)device reportIDFilter:(NSArray *) filter serialSuffix:(NSString *)suffix hacks:(NSDictionary *)hacks +{ + self = [super init]; + if (!self) return self; + + _physicallyConnected = true; + _logicallyConnected = true; + _device = (IOHIDDeviceRef)CFRetain(device); + _serialSuffix = suffix; + _playerLEDs = -1; + + IOHIDDeviceRegisterInputValueCallback(device, HIDInput, (void *)self); + IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + + NSArray *array = CFBridgingRelease(IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone)); + _buttons = [NSMutableDictionary dictionary]; + _axes = [NSMutableDictionary dictionary]; + _axes2D = [NSMutableDictionary dictionary]; + _hats = [NSMutableDictionary dictionary]; + _axisEmulatedButtons = [NSMutableDictionary dictionary]; + _axes2DEmulatedButtons = [NSMutableDictionary dictionary]; + _hatEmulatedButtons = [NSMutableDictionary dictionary]; + _iokitToJOY = [NSMutableDictionary dictionary]; + + + //NSMutableArray *axes3d = [NSMutableArray array]; + + _hacks = hacks; + _isSwitch = [_hacks[JOYIsSwitch] boolValue]; + _isDualShock3 = [_hacks[JOYIsDualShock3] boolValue]; + + NSDictionary *customReports = hacks[JOYCustomReports]; + _lastReport = [NSMutableData dataWithLength:MAX( + MAX( + [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxInputReportSizeKey)) unsignedIntValue], + [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxOutputReportSizeKey)) unsignedIntValue] + ), + [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxFeatureReportSizeKey)) unsignedIntValue] + )]; + IOHIDDeviceRegisterInputReportCallback(device, _lastReport.mutableBytes, _lastReport.length, HIDReport, (void *)self); + + if (hacks[JOYCustomReports]) { + _multiElements = [NSMutableDictionary dictionary]; + _fullReportElements = [NSMutableDictionary dictionary]; + + + for (NSNumber *_reportID in customReports) { + signed reportID = [_reportID intValue]; + bool isOutput = false; + if (reportID < 0) { + isOutput = true; + reportID = -reportID; + } + + JOYFullReportElement *element = [[JOYFullReportElement alloc] initWithDevice:device reportID:reportID]; + NSMutableArray *elements = [NSMutableArray array]; + for (NSDictionary *subElementDef in customReports[_reportID]) { + if (filter && subElementDef[@"reportID"] && ![filter containsObject:subElementDef[@"reportID"]]) continue; + JOYSubElement *subElement = [[JOYSubElement alloc] initWithRealElement:element + size:subElementDef[@"size"].unsignedLongValue + offset:subElementDef[@"offset"].unsignedLongValue + 8 // Compensate for the reportID + usagePage:subElementDef[@"usagePage"].unsignedLongValue + usage:subElementDef[@"usage"].unsignedLongValue + min:subElementDef[@"min"].unsignedIntValue + max:subElementDef[@"max"].unsignedIntValue]; + [elements addObject:subElement]; + if (isOutput) { + [self createOutputForElement:subElement]; + } + else { + [self createInputForElement:subElement]; + } + } + _multiElements[element] = elements; + if (!isOutput) { + _fullReportElements[@(reportID)] = element; + } + } + } + + id previous = nil; + NSSet *ignoredReports = nil; + if (hacks[ignoredReports]) { + ignoredReports = [NSSet setWithArray:hacks[ignoredReports]]; + } + + for (id _element in array) { + if (_element == previous) continue; // Some elements are reported twice for some reason + previous = _element; + JOYElement *element = [[JOYElement alloc] initWithElement:(__bridge IOHIDElementRef)_element]; + + bool isOutput = false; + if (filter && ![filter containsObject:@(element.reportID)]) continue; + + switch (IOHIDElementGetType((__bridge IOHIDElementRef)_element)) { + /* Handled */ + case kIOHIDElementTypeInput_Misc: + case kIOHIDElementTypeInput_Button: + case kIOHIDElementTypeInput_Axis: + break; + case kIOHIDElementTypeOutput: + isOutput = true; + break; + /* Ignored */ + default: + case kIOHIDElementTypeInput_ScanCodes: + case kIOHIDElementTypeInput_NULL: + case kIOHIDElementTypeFeature: + case kIOHIDElementTypeCollection: + continue; + } + if ((!isOutput && [ignoredReports containsObject:@(element.reportID)]) || + (isOutput && [ignoredReports containsObject:@(-element.reportID)])) continue; + + + if (IOHIDElementIsArray((__bridge IOHIDElementRef)_element)) continue; + + if (isOutput) { + [self createOutputForElement:element]; + } + else { + [self createInputForElement:element]; + } + + _iokitToJOY[@(IOHIDElementGetCookie((__bridge IOHIDElementRef)_element))] = element; + } + + [exposedControllers addObject:self]; + if (_logicallyConnected) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerConnected:)]) { + [listener controllerConnected:self]; + } + } + } + + if (_hacks[JOYActivationReport]) { + [self sendReport:hacks[JOYActivationReport]]; + } + + if (_isSwitch) { + [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x04} length:2]]; + [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x02} length:2]]; + } + + if (_isDualShock3) { + _lastVendorSpecificOutput.ds3Output = (JOYDualShock3Output){ + .reportID = 1, + .led = { + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0, .dutyLength = 0, .enabled = 0, .dutyOff = 0, .dutyOn = 0}, + } + }; + + } + + return self; +} + +- (NSString *)deviceName +{ + if (!_device) return nil; + return IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey)); +} + +- (NSString *)uniqueID +{ + if (!_device) return nil; + NSString *serial = (__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDSerialNumberKey)); + if (!serial || [(__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDTransportKey)) isEqualToString:@"USB"]) { + serial = [NSString stringWithFormat:@"%04x%04x%08x", + [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDVendorIDKey)) unsignedIntValue], + [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductIDKey)) unsignedIntValue], + [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDLocationIDKey)) unsignedIntValue]]; + } + if (_serialSuffix) { + return [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix]; + } + return serial; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p, %@, %@>", self.className, self, self.deviceName, self.uniqueID]; +} + +- (NSArray *)buttons +{ + NSMutableArray *ret = [[_buttons allValues] mutableCopy]; + [ret addObjectsFromArray:_axisEmulatedButtons.allValues]; + for (NSArray *array in _axes2DEmulatedButtons.allValues) { + [ret addObjectsFromArray:array]; + } + for (NSArray *array in _hatEmulatedButtons.allValues) { + [ret addObjectsFromArray:array]; + } + return ret; +} + +- (NSArray *)axes +{ + return [_axes allValues]; +} + +- (NSArray *)axes2D +{ + return [[NSSet setWithArray:[_axes2D allValues]] allObjects]; +} + +- (NSArray *)hats +{ + return [_hats allValues]; +} + +- (void)gotReport:(NSData *)report +{ + JOYFullReportElement *element = _fullReportElements[@(*(uint8_t *)report.bytes)]; + if (element) { + [element updateValue:report]; + + NSArray *subElements = _multiElements[element]; + if (subElements) { + for (JOYElement *subElement in subElements) { + [self _elementChanged:subElement]; + } + } + } + [self updateRumble]; +} + +- (void)elementChanged:(IOHIDElementRef)element +{ + JOYElement *_element = _iokitToJOY[@(IOHIDElementGetCookie(element))]; + if (_element) { + [self _elementChanged:_element]; + } + else { + //NSLog(@"Unhandled usage %x (Cookie: %x, Usage: %x)", IOHIDElementGetUsage(element), IOHIDElementGetCookie(element), IOHIDElementGetUsage(element)); + } +} + +- (void)_elementChanged:(JOYElement *)element +{ + if (element == _connectedElement) { + bool old = self.connected; + _logicallyConnected = _connectedElement.value != _connectedElement.min; + if (!old && self.connected) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerConnected:)]) { + [listener controllerConnected:self]; + } + } + } + else if (old && !self.connected) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { + [listener controllerDisconnected:self]; + } + } + } + } + + if (!self.connected) return; + { + JOYButton *button = _buttons[element]; + if (button) { + if ([button updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + return; + } + } + + + { + JOYAxis *axis = _axes[element]; + if (axis) { + if ([axis updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedAxis:)]) { + [listener controller:self movedAxis:axis]; + } + } + JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID)]; + if ([button updateStateFromAxis:axis]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + } + return; + } + } + + { + JOYAxes2D *axes = _axes2D[element]; + if (axes) { + if ([axes updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedAxes2D:)]) { + [listener controller:self movedAxes2D:axes]; + } + } + NSArray *buttons = _axes2DEmulatedButtons[@(axes.uniqueID)]; + for (JOYEmulatedButton *button in buttons) { + if ([button updateStateFromAxes2D:axes]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + } + } + return; + } + } + + { + JOYHat *hat = _hats[element]; + if (hat) { + if ([hat updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedHat:)]) { + [listener controller:self movedHat:hat]; + } + } + + NSArray *buttons = _hatEmulatedButtons[@(hat.uniqueID)]; + for (JOYEmulatedButton *button in buttons) { + if ([button updateStateFromHat:hat]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { + [listener controller:self buttonChangedState:button]; + } + } + } + } + } + return; + } + } +} + +- (void)disconnected +{ + if (_logicallyConnected && [exposedControllers containsObject:self]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { + [listener controllerDisconnected:self]; + } + } + } + _physicallyConnected = false; + [exposedControllers removeObject:self]; + [self setRumbleAmplitude:0]; + [self updateRumble]; + _device = nil; +} + +- (void)sendReport:(NSData *)report +{ + if (!report.length) return; + if (!_device) return; + if (_deviceCantSendReports) return; + /* Some Macs fail to send reports to some devices, specifically the DS3, returning the bogus(?) error code 1 after + freezing for 5 seconds. Stop sending reports if that's the case. */ + if (IOHIDDeviceSetReport(_device, kIOHIDReportTypeOutput, *(uint8_t *)report.bytes, report.bytes, report.length) == 1) { + _deviceCantSendReports = true; + NSLog(@"This Mac appears to be incapable of sending output reports to %@", self); + } +} + +- (void)setPlayerLEDs:(uint8_t)mask +{ + mask &= 0xF; + if (mask == _playerLEDs) { + return; + } + _playerLEDs = mask; + if (_isSwitch) { + _lastVendorSpecificOutput.switchPacket.reportID = 0x1; // Rumble and LEDs + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 0x30; // LED + _lastVendorSpecificOutput.switchPacket.commandData[0] = mask; + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + } + else if (_isDualShock3) { + _lastVendorSpecificOutput.ds3Output.reportID = 1; + _lastVendorSpecificOutput.ds3Output.ledsEnabled = mask << 1; + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]]; + } +} + +- (void)updateRumble +{ + if (!self.connected) { + return; + } + if (!_rumbleElement && !_isSwitch && !_isDualShock3) { + return; + } + if (_rumbleElement.max == 1 && _rumbleElement.min == 0) { + double ampToSend = _rumbleCounter < round(_rumbleAmplitude * PWM_RESOLUTION); + if (ampToSend != _sentRumbleAmp) { + [_rumbleElement setValue:ampToSend]; + _sentRumbleAmp = ampToSend; + } + _rumbleCounter += round(_rumbleAmplitude * PWM_RESOLUTION); + if (_rumbleCounter >= PWM_RESOLUTION) { + _rumbleCounter -= PWM_RESOLUTION; + } + } + else { + if (_rumbleAmplitude == _sentRumbleAmp) { + return; + } + _sentRumbleAmp = _rumbleAmplitude; + if (_isSwitch) { + double frequency = 144; + double amp = _rumbleAmplitude; + + uint8_t highAmp = amp * 0x64; + uint8_t lowAmp = amp * 0x32 + 0x40; + if (frequency < 0) frequency = 0; + if (frequency > 1252) frequency = 1252; + uint8_t encodedFrequency = (uint8_t)round(log2(frequency / 10.0) * 32.0); + + uint16_t highFreq = (encodedFrequency - 0x60) * 4; + uint8_t lowFreq = encodedFrequency - 0x40; + + //if (frequency < 82 || frequency > 312) { + if (amp) { + highAmp = 0; + } + + if (frequency < 40 || frequency > 626) { + lowAmp = 0; + } + + _lastVendorSpecificOutput.switchPacket.rumbleData[0] = _lastVendorSpecificOutput.switchPacket.rumbleData[4] = highFreq & 0xFF; + _lastVendorSpecificOutput.switchPacket.rumbleData[1] = _lastVendorSpecificOutput.switchPacket.rumbleData[5] = (highAmp << 1) + ((highFreq >> 8) & 0x1); + _lastVendorSpecificOutput.switchPacket.rumbleData[2] = _lastVendorSpecificOutput.switchPacket.rumbleData[6] = lowFreq; + _lastVendorSpecificOutput.switchPacket.rumbleData[3] = _lastVendorSpecificOutput.switchPacket.rumbleData[7] = lowAmp; + + + _lastVendorSpecificOutput.switchPacket.reportID = 0x10; // Rumble only + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 0; // LED + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + } + else if (_isDualShock3) { + _lastVendorSpecificOutput.ds3Output.reportID = 1; + _lastVendorSpecificOutput.ds3Output.rumbleLeftDuration = _lastVendorSpecificOutput.ds3Output.rumbleRightDuration = _rumbleAmplitude? 0xff : 0; + _lastVendorSpecificOutput.ds3Output.rumbleLeftStrength = _lastVendorSpecificOutput.ds3Output.rumbleRightStrength = round(_rumbleAmplitude * 0xff); + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]]; + } + else { + [_rumbleElement setValue:_rumbleAmplitude * (_rumbleElement.max - _rumbleElement.min) + _rumbleElement.min]; + } + } +} + +- (void)setRumbleAmplitude:(double)amp /* andFrequency: (double)frequency */ +{ + if (amp < 0) amp = 0; + if (amp > 1) amp = 1; + _rumbleAmplitude = amp; +} + +- (bool)isConnected +{ + return _logicallyConnected && _physicallyConnected; +} + ++ (void)controllerAdded:(IOHIDDeviceRef) device +{ + NSString *name = (__bridge NSString *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey)); + NSDictionary *hacks = hacksByName[name]; + if (!hacks) { + hacks = hacksByManufacturer[(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey))]; + } + NSArray *filters = hacks[JOYReportIDFilters]; + JOYController *controller = nil; + if (filters) { + controller = [[JOYMultiplayerController alloc] initWithDevice:device + reportIDFilters:filters + hacks:hacks]; + } + else { + controller = [[JOYController alloc] initWithDevice:device hacks:hacks]; + } + + [controllers setObject:controller forKey:[NSValue valueWithPointer:device]]; + + +} + ++ (void)controllerRemoved:(IOHIDDeviceRef) device +{ + [[controllers objectForKey:[NSValue valueWithPointer:device]] disconnected]; + [controllers removeObjectForKey:[NSValue valueWithPointer:device]]; +} + ++ (NSArray *)allControllers +{ + return exposedControllers; +} + ++ (void)load +{ +#include "ControllerConfiguration.inc" +} + ++(void)registerListener:(id)listener +{ + [listeners addObject:listener]; +} + ++(void)unregisterListener:(id)listener +{ + [listeners removeObject:listener]; +} + ++ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options +{ + axesEmulateButtons = [options[JOYAxesEmulateButtonsKey] boolValue]; + axes2DEmulateButtons = [options[JOYAxes2DEmulateButtonsKey] boolValue]; + hatsEmulateButtons = [options[JOYHatsEmulateButtonsKey] boolValue]; + + controllers = [NSMutableDictionary dictionary]; + exposedControllers = [NSMutableArray array]; + NSArray *array = @[ + CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick), + CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad), + CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController), + @{@kIOHIDDeviceUsagePageKey: @(kHIDPage_Game)}, + ]; + + listeners = [NSMutableSet set]; + static IOHIDManagerRef manager = nil; + if (manager) { + CFRelease(manager); // Stop the previous session + } + manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + + if (!manager) return; + if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone)) { + CFRelease(manager); + return; + } + + IOHIDManagerSetDeviceMatchingMultiple(manager, (__bridge CFArrayRef)array); + IOHIDManagerRegisterDeviceMatchingCallback(manager, HIDDeviceAdded, NULL); + IOHIDManagerRegisterDeviceRemovalCallback(manager, HIDDeviceRemoved, NULL); + IOHIDManagerScheduleWithRunLoop(manager, [runloop getCFRunLoop], kCFRunLoopDefaultMode); +} + +- (void)dealloc +{ + if (_device) { + CFRelease(_device); + _device = NULL; + } +} +@end diff --git a/bsnes/gb/JoyKit/JOYElement.h b/bsnes/gb/JoyKit/JOYElement.h new file mode 100644 index 00000000..0e917dd0 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYElement.h @@ -0,0 +1,20 @@ +#import +#include + +@interface JOYElement : NSObject +- (instancetype)initWithElement:(IOHIDElementRef)element; +- (int32_t)value; +- (NSData *)dataValue; +- (IOReturn)setValue:(uint32_t)value; +- (IOReturn)setDataValue:(NSData *)value; +@property (readonly) uint16_t usage; +@property (readonly) uint16_t usagePage; +@property (readonly) uint32_t uniqueID; +@property int32_t min; +@property int32_t max; +@property (readonly) int32_t reportID; +@property (readonly) int32_t parentID; + +@end + + diff --git a/bsnes/gb/JoyKit/JOYElement.m b/bsnes/gb/JoyKit/JOYElement.m new file mode 100644 index 00000000..2432002a --- /dev/null +++ b/bsnes/gb/JoyKit/JOYElement.m @@ -0,0 +1,133 @@ +#import "JOYElement.h" +#include +#include + +@implementation JOYElement +{ + id _element; + IOHIDDeviceRef _device; + int32_t _min, _max; +} + +- (int32_t)min +{ + return MIN(_min, _max); +} + +- (int32_t)max +{ + return MAX(_max, _min); +} + +-(void)setMin:(int32_t)min +{ + _min = min; +} + +- (void)setMax:(int32_t)max +{ + _max = max; +} + +/* Ugly hack because IOHIDDeviceCopyMatchingElements is slow */ ++ (NSArray *) cookiesToSkipForDevice:(IOHIDDeviceRef)device +{ + id _device = (__bridge id)device; + NSMutableArray *ret = objc_getAssociatedObject(_device, _cmd); + if (ret) return ret; + + ret = [NSMutableArray array]; + NSArray *nones = CFBridgingRelease(IOHIDDeviceCopyMatchingElements(device, + (__bridge CFDictionaryRef)@{@(kIOHIDElementTypeKey): @(kIOHIDElementTypeInput_NULL)}, + 0)); + for (id none in nones) { + [ret addObject:@(IOHIDElementGetCookie((__bridge IOHIDElementRef)none))]; + } + objc_setAssociatedObject(_device, _cmd, ret, OBJC_ASSOCIATION_RETAIN); + return ret; +} + +- (instancetype)initWithElement:(IOHIDElementRef)element +{ + if ((self = [super init])) { + _element = (__bridge id)element; + _usage = IOHIDElementGetUsage(element); + _usagePage = IOHIDElementGetUsagePage(element); + _uniqueID = (uint32_t)IOHIDElementGetCookie(element); + _min = (int32_t) IOHIDElementGetLogicalMin(element); + _max = (int32_t) IOHIDElementGetLogicalMax(element); + _reportID = IOHIDElementGetReportID(element); + IOHIDElementRef parent = IOHIDElementGetParent(element); + _parentID = parent? (uint32_t)IOHIDElementGetCookie(parent) : -1; + _device = IOHIDElementGetDevice(element); + + /* Catalina added a new input type in a way that breaks cookie consistency across macOS versions, + we shall adjust our cookies to to compensate */ + unsigned cookieShift = 0, parentCookieShift = 0; + + for (NSNumber *none in [JOYElement cookiesToSkipForDevice:_device]) { + if (none.unsignedIntValue < _uniqueID) { + cookieShift++; + } + if (none.unsignedIntValue < (int32_t)_parentID) { + parentCookieShift++; + } + } + + _uniqueID -= cookieShift; + _parentID -= parentCookieShift; + } + return self; +} + +- (int32_t)value +{ + IOHIDValueRef value = NULL; + IOHIDDeviceGetValue(_device, (__bridge IOHIDElementRef)_element, &value); + if (!value) return 0; + CFRelease(CFRetain(value)); // For some reason, this is required to prevent leaks. + return (int32_t)IOHIDValueGetIntegerValue(value); +} + +- (NSData *)dataValue +{ + IOHIDValueRef value = NULL; + IOHIDDeviceGetValue(_device, (__bridge IOHIDElementRef)_element, &value); + if (!value) return 0; + CFRelease(CFRetain(value)); // For some reason, this is required to prevent leaks. + return [NSData dataWithBytes:IOHIDValueGetBytePtr(value) length:IOHIDValueGetLength(value)]; +} + +- (IOReturn)setValue:(uint32_t)value +{ + IOHIDValueRef ivalue = IOHIDValueCreateWithIntegerValue(NULL, (__bridge IOHIDElementRef)_element, 0, value); + IOReturn ret = IOHIDDeviceSetValue(_device, (__bridge IOHIDElementRef)_element, ivalue); + CFRelease(ivalue); + return ret; +} + +- (IOReturn)setDataValue:(NSData *)value +{ + IOHIDValueRef ivalue = IOHIDValueCreateWithBytes(NULL, (__bridge IOHIDElementRef)_element, 0, value.bytes, value.length); + IOReturn ret = IOHIDDeviceSetValue(_device, (__bridge IOHIDElementRef)_element, ivalue); + CFRelease(ivalue); + return ret; +} + +/* For use as a dictionary key */ + +- (NSUInteger)hash +{ + return self.uniqueID; +} + +- (BOOL)isEqual:(id)object +{ + return self->_element == object; +} + +- (id)copyWithZone:(nullable NSZone *)zone; +{ + return self; +} +@end diff --git a/bsnes/gb/JoyKit/JOYEmulatedButton.h b/bsnes/gb/JoyKit/JOYEmulatedButton.h new file mode 100644 index 00000000..491e0c73 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYEmulatedButton.h @@ -0,0 +1,11 @@ +#import "JOYButton.h" +#import "JOYAxis.h" +#import "JOYAxes2D.h" +#import "JOYHat.h" + +@interface JOYEmulatedButton : JOYButton +- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +- (bool)updateStateFromAxis:(JOYAxis *)axis; +- (bool)updateStateFromAxes2D:(JOYAxes2D *)axes; +- (bool)updateStateFromHat:(JOYHat *)hat; +@end diff --git a/bsnes/gb/JoyKit/JOYEmulatedButton.m b/bsnes/gb/JoyKit/JOYEmulatedButton.m new file mode 100644 index 00000000..1ebed3ae --- /dev/null +++ b/bsnes/gb/JoyKit/JOYEmulatedButton.m @@ -0,0 +1,91 @@ +#import "JOYEmulatedButton.h" + +@interface JOYButton () +{ + @public bool _state; +} +@end + +@implementation JOYEmulatedButton +{ + uint64_t _uniqueID; +} + +- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +{ + self = [super init]; + self.usage = usage; + _uniqueID = uniqueID; + + return self; +} + +- (uint64_t)uniqueID +{ + return _uniqueID; +} + +- (bool)updateStateFromAxis:(JOYAxis *)axis +{ + bool old = _state; + _state = [axis value] > 0.5; + return _state != old; +} + +- (bool)updateStateFromAxes2D:(JOYAxes2D *)axes +{ + bool old = _state; + if (axes.distance < 0.5) { + _state = false; + } + else { + unsigned direction = ((unsigned)round(axes.angle / 360 * 8)) & 7; + switch (self.usage) { + case JOYButtonUsageDPadLeft: + _state = direction >= 3 && direction <= 5; + break; + case JOYButtonUsageDPadRight: + _state = direction <= 1 || direction == 7; + break; + case JOYButtonUsageDPadUp: + _state = direction >= 5; + break; + case JOYButtonUsageDPadDown: + _state = direction <= 3 && direction >= 1; + break; + default: + break; + } + } + return _state != old; +} + +- (bool)updateStateFromHat:(JOYHat *)hat +{ + bool old = _state; + if (!hat.pressed) { + _state = false; + } + else { + unsigned direction = ((unsigned)round(hat.angle / 360 * 8)) & 7; + switch (self.usage) { + case JOYButtonUsageDPadLeft: + _state = direction >= 3 && direction <= 5; + break; + case JOYButtonUsageDPadRight: + _state = direction <= 1 || direction == 7; + break; + case JOYButtonUsageDPadUp: + _state = direction >= 5; + break; + case JOYButtonUsageDPadDown: + _state = direction <= 3 && direction >= 1; + break; + default: + break; + } + } + return _state != old; +} + +@end diff --git a/bsnes/gb/JoyKit/JOYFullReportElement.h b/bsnes/gb/JoyKit/JOYFullReportElement.h new file mode 100644 index 00000000..808644e7 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYFullReportElement.h @@ -0,0 +1,10 @@ +#import +#include +#include "JOYElement.h" + +@interface JOYFullReportElement : JOYElement +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportID:(unsigned)reportID; +- (void)updateValue:(NSData *)value; +@end + + diff --git a/bsnes/gb/JoyKit/JOYFullReportElement.m b/bsnes/gb/JoyKit/JOYFullReportElement.m new file mode 100644 index 00000000..c8efb270 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYFullReportElement.m @@ -0,0 +1,73 @@ +#import "JOYFullReportElement.h" +#include + +@implementation JOYFullReportElement +{ + IOHIDDeviceRef _device; + NSData *_data; + unsigned _reportID; + size_t _capacity; +} + +- (uint32_t)uniqueID +{ + return _reportID ^ 0xFFFF; +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportID:(unsigned)reportID +{ + if ((self = [super init])) { + _data = [[NSMutableData alloc] initWithLength:[(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDMaxOutputReportSizeKey)) unsignedIntValue]]; + *(uint8_t *)(((NSMutableData *)_data).mutableBytes) = reportID; + _reportID = reportID; + _device = device; + } + return self; +} + +- (int32_t)value +{ + [self doesNotRecognizeSelector:_cmd]; + return 0; +} + +- (NSData *)dataValue +{ + return _data; +} + +- (IOReturn)setValue:(uint32_t)value +{ + [self doesNotRecognizeSelector:_cmd]; + return -1; +} + +- (IOReturn)setDataValue:(NSData *)value +{ + + [self updateValue:value]; + return IOHIDDeviceSetReport(_device, kIOHIDReportTypeOutput, _reportID, [_data bytes], [_data length]);; +} + +- (void)updateValue:(NSData *)value +{ + _data = [value copy]; +} + +/* For use as a dictionary key */ + +- (NSUInteger)hash +{ + return self.uniqueID; +} + +- (BOOL)isEqual:(id)object +{ + return self.uniqueID == self.uniqueID; +} + +- (id)copyWithZone:(nullable NSZone *)zone; +{ + return self; +} +@end diff --git a/bsnes/gb/JoyKit/JOYHat.h b/bsnes/gb/JoyKit/JOYHat.h new file mode 100644 index 00000000..05a58292 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYHat.h @@ -0,0 +1,11 @@ +#import + +@interface JOYHat : NSObject +- (uint64_t)uniqueID; +- (double)angle; +- (unsigned)resolution; +@property (readonly, getter=isPressed) bool pressed; + +@end + + diff --git a/bsnes/gb/JoyKit/JOYHat.m b/bsnes/gb/JoyKit/JOYHat.m new file mode 100644 index 00000000..743e49c3 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYHat.m @@ -0,0 +1,60 @@ +#import "JOYHat.h" +#import "JOYElement.h" + +@implementation JOYHat +{ + JOYElement *_element; + double _state; +} + +- (uint64_t)uniqueID +{ + return _element.uniqueID; +} + +- (NSString *)description +{ + if (self.isPressed) { + return [NSString stringWithFormat:@"<%@: %p (%llu); State: %f degrees>", self.className, self, self.uniqueID, self.angle]; + } + return [NSString stringWithFormat:@"<%@: %p (%llu); State: released>", self.className, self, self.uniqueID]; + +} + +- (instancetype)initWithElement:(JOYElement *)element +{ + self = [super init]; + if (!self) return self; + + _element = element; + + return self; +} + +- (bool)isPressed +{ + return _state >= 0 && _state < 360; +} + +- (double)angle +{ + if (self.isPressed) return fmod((_state + 270), 360); + return -1; +} + +- (unsigned)resolution +{ + return _element.max - _element.min + 1; +} + +- (bool)updateState +{ + unsigned state = ([_element value] - _element.min) * 360.0 / self.resolution; + if (_state != state) { + _state = state; + return true; + } + return false; +} + +@end diff --git a/bsnes/gb/JoyKit/JOYMultiplayerController.h b/bsnes/gb/JoyKit/JOYMultiplayerController.h new file mode 100644 index 00000000..44d74219 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYMultiplayerController.h @@ -0,0 +1,8 @@ +#import "JOYController.h" +#include + +@interface JOYMultiplayerController : JOYController +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportIDFilters:(NSArray *>*) reportIDFilters hacks:hacks; +@end + + diff --git a/bsnes/gb/JoyKit/JOYMultiplayerController.m b/bsnes/gb/JoyKit/JOYMultiplayerController.m new file mode 100644 index 00000000..a31ae921 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYMultiplayerController.m @@ -0,0 +1,50 @@ +#import "JOYMultiplayerController.h" + +@interface JOYController () +- (instancetype)initWithDevice:(IOHIDDeviceRef)device reportIDFilter:(NSArray *) filter serialSuffix:(NSString *)suffix hacks:(NSDictionary *)hacks; +- (void)elementChanged:(IOHIDElementRef) element toValue:(IOHIDValueRef) value; +- (void)disconnected; +- (void)sendReport:(NSData *)report; +@end + +@implementation JOYMultiplayerController +{ + NSMutableArray *_children; +} + +- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportIDFilters:(NSArray *>*) reportIDFilters hacks:(NSDictionary *)hacks; +{ + self = [super init]; + if (!self) return self; + + _children = [NSMutableArray array]; + + unsigned index = 1; + for (NSArray *filter in reportIDFilters) { + JOYController *controller = [[JOYController alloc] initWithDevice:device reportIDFilter:filter serialSuffix:[NSString stringWithFormat:@"%d", index] hacks:hacks]; + [_children addObject:controller]; + index++; + } + return self; +} + +- (void)elementChanged:(IOHIDElementRef) element toValue:(IOHIDValueRef) value +{ + for (JOYController *child in _children) { + [child elementChanged:element toValue:value]; + } +} + +- (void)disconnected +{ + for (JOYController *child in _children) { + [child disconnected]; + } +} + +- (void)sendReport:(NSData *)report +{ + [[_children firstObject] sendReport:report]; +} + +@end diff --git a/bsnes/gb/JoyKit/JOYSubElement.h b/bsnes/gb/JoyKit/JOYSubElement.h new file mode 100644 index 00000000..a13b5c76 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYSubElement.h @@ -0,0 +1,14 @@ +#import "JOYElement.h" + +@interface JOYSubElement : JOYElement +- (instancetype)initWithRealElement:(JOYElement *)element + size:(size_t) size // in bits + offset:(size_t) offset // in bits + usagePage:(uint16_t)usagePage + usage:(uint16_t)usage + min:(int32_t)min + max:(int32_t)max; + +@end + + diff --git a/bsnes/gb/JoyKit/JOYSubElement.m b/bsnes/gb/JoyKit/JOYSubElement.m new file mode 100644 index 00000000..c94badc7 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYSubElement.m @@ -0,0 +1,100 @@ +#import "JOYSubElement.h" + +@interface JOYElement () +{ + @public uint16_t _usage; + @public uint16_t _usagePage; + @public uint32_t _uniqueID; + @public int32_t _min; + @public int32_t _max; + @public int32_t _reportID; + @public int32_t _parentID; +} +@end + +@implementation JOYSubElement +{ + JOYElement *_parent; + size_t _size; // in bits + size_t _offset; // in bits +} + +- (instancetype)initWithRealElement:(JOYElement *)element + size:(size_t) size // in bits + offset:(size_t) offset // in bits + usagePage:(uint16_t)usagePage + usage:(uint16_t)usage + min:(int32_t)min + max:(int32_t)max +{ + if ((self = [super init])) { + _parent = element; + _size = size; + _offset = offset; + _usage = usage; + _usagePage = usagePage; + _uniqueID = (uint32_t)((_parent.uniqueID << 16) | offset); + _min = min; + _max = max; + _reportID = _parent.reportID; + _parentID = _parent.parentID; + } + return self; +} + +- (int32_t)value +{ + NSData *parentValue = [_parent dataValue]; + if (!parentValue) return 0; + if (_size > 32) return 0; + if (_size + (_offset % 8) > 32) return 0; + size_t parentLength = parentValue.length; + if (_size > parentLength * 8) return 0; + if (_size + _offset >= parentLength * 8) return 0; + const uint8_t *bytes = parentValue.bytes; + + uint8_t temp[4] = {0,}; + memcpy(temp, bytes + _offset / 8, (_offset + _size - 1) / 8 - _offset / 8 + 1); + uint32_t ret = (*(uint32_t *)temp) >> (_offset % 8); + ret &= (1 << _size) - 1; + + if (_max < _min) { + return _max + _min - ret; + } + + return ret; +} + +- (IOReturn)setValue: (uint32_t) value +{ + NSMutableData *dataValue = [[_parent dataValue] mutableCopy]; + if (!dataValue) return -1; + if (_size > 32) return -1; + if (_size + (_offset % 8) > 32) return -1; + size_t parentLength = dataValue.length; + if (_size > parentLength * 8) return -1; + if (_size + _offset >= parentLength * 8) return -1; + uint8_t *bytes = dataValue.mutableBytes; + + uint8_t temp[4] = {0,}; + memcpy(temp, bytes + _offset / 8, (_offset + _size - 1) / 8 - _offset / 8 + 1); + (*(uint32_t *)temp) &= ~((1 << (_size - 1)) << (_offset % 8)); + (*(uint32_t *)temp) |= (value) << (_offset % 8); + memcpy(bytes + _offset / 8, temp, (_offset + _size - 1) / 8 - _offset / 8 + 1); + return [_parent setDataValue:dataValue]; +} + +- (NSData *)dataValue +{ + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (IOReturn)setDataValue:(NSData *)data +{ + [self doesNotRecognizeSelector:_cmd]; + return -1; +} + + +@end diff --git a/bsnes/gb/JoyKit/JoyKit.h b/bsnes/gb/JoyKit/JoyKit.h new file mode 100644 index 00000000..d56b5051 --- /dev/null +++ b/bsnes/gb/JoyKit/JoyKit.h @@ -0,0 +1,6 @@ +#ifndef JoyKit_h +#define JoyKit_h + +#include "JOYController.h" + +#endif diff --git a/bsnes/gb/LICENSE b/bsnes/gb/LICENSE new file mode 100644 index 00000000..17619e99 --- /dev/null +++ b/bsnes/gb/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2020 Lior Halphon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +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/bsnes/gb/Makefile b/bsnes/gb/Makefile new file mode 100644 index 00000000..c3d030aa --- /dev/null +++ b/bsnes/gb/Makefile @@ -0,0 +1,428 @@ +# Make hacks +.INTERMEDIATE: + +# Set target, configuration, version and destination folders + +PLATFORM := $(shell uname -s) +ifneq ($(findstring MINGW,$(PLATFORM)),) +PLATFORM := windows32 +USE_WINDRES := true +endif + +ifneq ($(findstring MSYS,$(PLATFORM)),) +PLATFORM := windows32 +endif + +ifeq ($(PLATFORM),windows32) +_ := $(shell chcp 65001) +EXESUFFIX:=.exe +NATIVE_CC = clang -IWindows -Wno-deprecated-declarations --target=i386-pc-windows +else +EXESUFFIX:= +NATIVE_CC := cc +endif + +PB12_COMPRESS := build/pb12$(EXESUFFIX) + +ifeq ($(PLATFORM),Darwin) +DEFAULT := cocoa +else +DEFAULT := sdl +endif + +default: $(DEFAULT) + +ifeq ($(MAKECMDGOALS),) +MAKECMDGOALS := $(DEFAULT) +endif + +include version.mk +export VERSION +CONF ?= debug +SDL_AUDIO_DRIVER ?= sdl + +BIN := build/bin +OBJ := build/obj +BOOTROMS_DIR ?= $(BIN)/BootROMs + +ifdef DATA_DIR +CFLAGS += -DDATA_DIR="\"$(DATA_DIR)\"" +endif + +# Set tools + +# Use clang if it's available. +ifeq ($(origin CC),default) +ifneq (, $(shell which clang)) +CC := clang +endif +endif + +# Find libraries with pkg-config if available. +ifneq (, $(shell which pkg-config)) +PKG_CONFIG := pkg-config +endif + +ifeq ($(PLATFORM),windows32) +# To force use of the Unix version instead of the Windows version +MKDIR := $(shell which mkdir) +else +MKDIR := mkdir +endif + +ifeq ($(CONF),native_release) +override CONF := release +LDFLAGS += -march=native -mtune=native +CFLAGS += -march=native -mtune=native +endif + +ifeq ($(CONF),fat_release) +override CONF := release +FAT_FLAGS += -arch x86_64 -arch arm64 +endif + + + +# Set compilation and linkage flags based on target, platform and configuration + +OPEN_DIALOG = OpenDialog/gtk.c +NULL := /dev/null + +ifeq ($(PLATFORM),windows32) +OPEN_DIALOG = OpenDialog/windows.c +NULL := NUL +endif + +ifeq ($(PLATFORM),Darwin) +OPEN_DIALOG = OpenDialog/cocoa.m +endif + +# These must come before the -Wno- flags +WARNINGS += -Werror -Wall -Wno-unknown-warning -Wno-unknown-warning-option +WARNINGS += -Wno-nonnull -Wno-unused-result -Wno-strict-aliasing -Wno-multichar -Wno-int-in-bool-context + +# Only add this flag if the compiler supports it +ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -Wpartial-availability 2> $(NULL); echo $$?),0) +WARNINGS += -Wpartial-availability +endif + +# GCC's implementation of this warning has false positives, so we skip it +ifneq ($(shell $(CC) --version 2>&1 | grep "gcc"), ) +WARNINGS += -Wno-maybe-uninitialized +endif + +CFLAGS += $(WARNINGS) + +CFLAGS += -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES + +ifeq (,$(PKG_CONFIG)) +SDL_CFLAGS := $(shell sdl2-config --cflags) +SDL_LDFLAGS := $(shell sdl2-config --libs) +else +SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2) +SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) +endif +ifeq (,$(PKG_CONFIG)) +GL_LDFLAGS := -lGL +else +GL_CFLAGS := $(shell $(PKG_CONFIG) --cflags gl) +GL_LDFLAGS := $(shell $(PKG_CONFIG) --libs gl || echo -lGL) +endif +ifeq ($(PLATFORM),windows32) +CFLAGS += -IWindows -Drandom=rand --target=i386-pc-windows +LDFLAGS += -lmsvcrt -lcomdlg32 -luser32 -lshell32 -lSDL2main -Wl,/MANIFESTFILE:NUL --target=i386-pc-windows +SDL_LDFLAGS := -lSDL2 +GL_LDFLAGS := -lopengl32 +else +LDFLAGS += -lc -lm -ldl +endif + +ifeq ($(PLATFORM),Darwin) +SYSROOT := $(shell xcodebuild -sdk macosx -version Path 2> $(NULL)) +ifeq ($(SYSROOT),) +SYSROOT := /Library/Developer/CommandLineTools/SDKs/$(shell ls /Library/Developer/CommandLineTools/SDKs/ | grep 10 | tail -n 1) +endif +ifeq ($(SYSROOT),/Library/Developer/CommandLineTools/SDKs/) +$(error Could not find a macOS SDK) +endif + +CFLAGS += -F/Library/Frameworks -mmacosx-version-min=10.9 -isysroot $(SYSROOT) +OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT) +LDFLAGS += -framework AppKit -framework PreferencePanes -framework Carbon -framework QuartzCore -weak_framework Metal -weak_framework MetalKit -mmacosx-version-min=10.9 -isysroot $(SYSROOT) +GL_LDFLAGS := -framework OpenGL +endif +CFLAGS += -Wno-deprecated-declarations +ifeq ($(PLATFORM),windows32) +CFLAGS += -Wno-deprecated-declarations # Seems like Microsoft deprecated every single LIBC function +LDFLAGS += -Wl,/NODEFAULTLIB:libcmt.lib +endif + +ifeq ($(CONF),debug) +CFLAGS += -g +else ifeq ($(CONF), release) +CFLAGS += -O3 -DNDEBUG +STRIP := strip +ifeq ($(PLATFORM),Darwin) +LDFLAGS += -Wl,-exported_symbols_list,$(NULL) +STRIP := -@true +endif +ifneq ($(PLATFORM),windows32) +LDFLAGS += -flto +CFLAGS += -flto +LDFLAGS += -Wno-lto-type-mismatch # For GCC's LTO +endif + +else +$(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release") +endif + +# Define our targets + +ifeq ($(PLATFORM),windows32) +SDL_TARGET := $(BIN)/SDL/sameboy.exe $(BIN)/SDL/sameboy_debugger.exe $(BIN)/SDL/SDL2.dll +TESTER_TARGET := $(BIN)/tester/sameboy_tester.exe +else +SDL_TARGET := $(BIN)/SDL/sameboy +TESTER_TARGET := $(BIN)/tester/sameboy_tester +endif + +cocoa: $(BIN)/SameBoy.app +quicklook: $(BIN)/SameBoy.qlgenerator +sdl: $(SDL_TARGET) $(BIN)/SDL/dmg_boot.bin $(BIN)/SDL/cgb_boot.bin $(BIN)/SDL/agb_boot.bin $(BIN)/SDL/sgb_boot.bin $(BIN)/SDL/sgb2_boot.bin $(BIN)/SDL/LICENSE $(BIN)/SDL/registers.sym $(BIN)/SDL/background.bmp $(BIN)/SDL/Shaders +bootroms: $(BIN)/BootROMs/agb_boot.bin $(BIN)/BootROMs/cgb_boot.bin $(BIN)/BootROMs/dmg_boot.bin $(BIN)/BootROMs/sgb_boot.bin $(BIN)/BootROMs/sgb2_boot.bin +tester: $(TESTER_TARGET) $(BIN)/tester/dmg_boot.bin $(BIN)/tester/cgb_boot.bin $(BIN)/tester/agb_boot.bin $(BIN)/tester/sgb_boot.bin $(BIN)/tester/sgb2_boot.bin +all: cocoa sdl tester libretro + +# Get a list of our source files and their respective object file targets + +CORE_SOURCES := $(shell ls Core/*.c) +SDL_SOURCES := $(shell ls SDL/*.c) $(OPEN_DIALOG) SDL/audio/$(SDL_AUDIO_DRIVER).c +TESTER_SOURCES := $(shell ls Tester/*.c) + +ifeq ($(PLATFORM),Darwin) +COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) $(shell ls JoyKit/*.m) +QUICKLOOK_SOURCES := $(shell ls QuickLook/*.m) $(shell ls QuickLook/*.c) +endif + +ifeq ($(PLATFORM),windows32) +CORE_SOURCES += $(shell ls Windows/*.c) +endif + +CORE_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(CORE_SOURCES)) +COCOA_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(COCOA_SOURCES)) +QUICKLOOK_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(QUICKLOOK_SOURCES)) +SDL_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(SDL_SOURCES)) +TESTER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(TESTER_SOURCES)) + +# Automatic dependency generation + +ifneq ($(filter-out clean bootroms libretro %.bin, $(MAKECMDGOALS)),) +-include $(CORE_OBJECTS:.o=.dep) +ifneq ($(filter $(MAKECMDGOALS),sdl),) +-include $(SDL_OBJECTS:.o=.dep) +endif +ifneq ($(filter $(MAKECMDGOALS),tester),) +-include $(TESTER_OBJECTS:.o=.dep) +endif +ifneq ($(filter $(MAKECMDGOALS),cocoa),) +-include $(COCOA_OBJECTS:.o=.dep) +endif +endif + +$(OBJ)/SDL/%.dep: SDL/% + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -MT $(OBJ)/$^.o -M $^ -c -o $@ + +$(OBJ)/%.dep: % + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) -MT $(OBJ)/$^.o -M $^ -c -o $@ + +# Compilation rules + +$(OBJ)/Core/%.c.o: Core/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(FAT_FLAGS) -DGB_INTERNAL -c $< -o $@ + +$(OBJ)/SDL/%.c.o: SDL/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(FAT_FLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ + +$(OBJ)/%.c.o: %.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(FAT_FLAGS) -c $< -o $@ + +# HexFiend requires more flags +$(OBJ)/HexFiend/%.m.o: HexFiend/%.m + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(FAT_FLAGS) $(OCFLAGS) -c $< -o $@ -fno-objc-arc -include HexFiend/HexFiend_2_Framework_Prefix.pch + +$(OBJ)/%.m.o: %.m + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(FAT_FLAGS) $(OCFLAGS) -c $< -o $@ + +# Cocoa Port + +$(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \ + $(shell ls Cocoa/*.icns Cocoa/*.png) \ + Cocoa/License.html \ + Cocoa/Info.plist \ + Misc/registers.sym \ + $(BIN)/SameBoy.app/Contents/Resources/dmg_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/cgb_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/agb_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/sgb_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/sgb2_boot.bin \ + $(patsubst %.xib,%.nib,$(addprefix $(BIN)/SameBoy.app/Contents/Resources/Base.lproj/,$(shell cd Cocoa;ls *.xib))) \ + $(BIN)/SameBoy.qlgenerator \ + Shaders + $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Resources + cp Cocoa/*.icns Cocoa/*.png Misc/registers.sym $(BIN)/SameBoy.app/Contents/Resources/ + sed s/@VERSION/$(VERSION)/ < Cocoa/Info.plist > $(BIN)/SameBoy.app/Contents/Info.plist + cp Cocoa/License.html $(BIN)/SameBoy.app/Contents/Resources/Credits.html + $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Resources/Shaders + cp Shaders/*.fsh Shaders/*.metal $(BIN)/SameBoy.app/Contents/Resources/Shaders + $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Library/QuickLook/ + cp -rf $(BIN)/SameBoy.qlgenerator $(BIN)/SameBoy.app/Contents/Library/QuickLook/ + +$(BIN)/SameBoy.app/Contents/MacOS/SameBoy: $(CORE_OBJECTS) $(COCOA_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework IOKit +ifeq ($(CONF), release) + $(STRIP) $@ +endif + +$(BIN)/SameBoy.app/Contents/Resources/Base.lproj/%.nib: Cocoa/%.xib + ibtool --compile $@ $^ 2>&1 | cat - + +# Quick Look generator + +$(BIN)/SameBoy.qlgenerator: $(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL \ + $(shell ls QuickLook/*.png) \ + QuickLook/Info.plist \ + $(BIN)/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin + $(MKDIR) -p $(BIN)/SameBoy.qlgenerator/Contents/Resources + cp QuickLook/*.png $(BIN)/SameBoy.qlgenerator/Contents/Resources/ + sed s/@VERSION/$(VERSION)/ < QuickLook/Info.plist > $(BIN)/SameBoy.qlgenerator/Contents/Info.plist + +# Currently, SameBoy.app includes two "copies" of each Core .o file once in the app itself and +# once in the QL Generator. It should probably become a dylib instead. +$(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL: $(CORE_OBJECTS) $(QUICKLOOK_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -Wl,-exported_symbols_list,QuickLook/exports.sym -bundle -framework Cocoa -framework Quicklook + +# cgb_boot_fast.bin is not a standard boot ROM, we don't expect it to exist in the user-provided +# boot ROM directory. +$(BIN)/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin: $(BIN)/BootROMs/cgb_boot_fast.bin + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +# SDL Port + +# Unix versions build only one binary +$(BIN)/SDL/sameboy: $(CORE_OBJECTS) $(SDL_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) +ifeq ($(CONF), release) + $(STRIP) $@ +endif + +# Windows version builds two, one with a conole and one without it +$(BIN)/SDL/sameboy.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/resources.o + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) -Wl,/subsystem:windows + +$(BIN)/SDL/sameboy_debugger.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/resources.o + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) -Wl,/subsystem:console + +ifneq ($(USE_WINDRES),) +$(OBJ)/%.o: %.rc + -@$(MKDIR) -p $(dir $@) + windres --preprocessor cpp -DVERSION=\"$(VERSION)\" $^ $@ +else +$(OBJ)/%.res: %.rc + -@$(MKDIR) -p $(dir $@) + rc /fo $@ /dVERSION=\"$(VERSION)\" $^ + +%.o: %.res + cvtres /OUT:"$@" $^ +endif + +# We must provide SDL2.dll with the Windows port. +$(BIN)/SDL/SDL2.dll: + @$(eval MATCH := $(shell where $$LIB:SDL2.dll)) + cp "$(MATCH)" $@ + +# Tester + +$(BIN)/tester/sameboy_tester: $(CORE_OBJECTS) $(TESTER_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) +ifeq ($(CONF), release) + $(STRIP) $@ +endif + +$(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) -Wl,/subsystem:console + +$(BIN)/SDL/%.bin: $(BOOTROMS_DIR)/%.bin + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +$(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +$(BIN)/SameBoy.app/Contents/Resources/%.bin: $(BOOTROMS_DIR)/%.bin + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +$(BIN)/SDL/LICENSE: LICENSE + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +$(BIN)/SDL/registers.sym: Misc/registers.sym + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +$(BIN)/SDL/background.bmp: SDL/background.bmp + -@$(MKDIR) -p $(dir $@) + cp -f $^ $@ + +$(BIN)/SDL/Shaders: Shaders + -@$(MKDIR) -p $@ + cp -rf Shaders/*.fsh $@ + +# Boot ROMs + +$(OBJ)/%.2bpp: %.png + -@$(MKDIR) -p $(dir $@) + rgbgfx -h -u -o $@ $< + +$(OBJ)/BootROMs/SameBoyLogo.pb12: $(OBJ)/BootROMs/SameBoyLogo.2bpp $(PB12_COMPRESS) + $(realpath $(PB12_COMPRESS)) < $< > $@ + +$(PB12_COMPRESS): BootROMs/pb12.c + $(NATIVE_CC) -std=c99 -Wall -Werror $< -o $@ + +$(BIN)/BootROMs/agb_boot.bin: BootROMs/cgb_boot.asm +$(BIN)/BootROMs/cgb_boot_fast.bin: BootROMs/cgb_boot.asm +$(BIN)/BootROMs/sgb2_boot: BootROMs/sgb_boot.asm + +$(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.pb12 + -@$(MKDIR) -p $(dir $@) + rgbasm -i $(OBJ)/BootROMs/ -i BootROMs/ -o $@.tmp $< + rgblink -o $@.tmp2 $@.tmp + dd if=$@.tmp2 of=$@ count=1 bs=$(if $(findstring dmg,$@)$(findstring sgb,$@),256,2304) 2> $(NULL) + @rm $@.tmp $@.tmp2 + +# Libretro Core (uses its own build system) +libretro: + CFLAGS="$(WARNINGS)" $(MAKE) -C libretro + +# Clean +clean: + rm -rf build + +.PHONY: libretro tester diff --git a/bsnes/gb/Misc/registers.sym b/bsnes/gb/Misc/registers.sym new file mode 100644 index 00000000..3b31b745 --- /dev/null +++ b/bsnes/gb/Misc/registers.sym @@ -0,0 +1,67 @@ +00:FF00 IO_JOYP +00:FF01 IO_SB +00:FF02 IO_SC +00:FF04 IO_DIV +00:FF05 IO_TIMA +00:FF06 IO_TMA +00:FF07 IO_TAC +00:FF0F IO_IF +00:FF10 IO_NR10 +00:FF11 IO_NR11 +00:FF12 IO_NR12 +00:FF13 IO_NR13 +00:FF14 IO_NR14 +00:FF16 IO_NR21 +00:FF17 IO_NR22 +00:FF18 IO_NR23 +00:FF19 IO_NR24 +00:FF1A IO_NR30 +00:FF1B IO_NR31 +00:FF1C IO_NR32 +00:FF1D IO_NR33 +00:FF1E IO_NR34 +00:FF20 IO_NR41 +00:FF21 IO_NR42 +00:FF22 IO_NR43 +00:FF23 IO_NR44 +00:FF24 IO_NR50 +00:FF25 IO_NR51 +00:FF26 IO_NR52 +00:FF30 IO_WAV_START +00:FF3F IO_WAV_END +00:FF40 IO_LCDC +00:FF41 IO_STAT +00:FF42 IO_SCY +00:FF43 IO_SCX +00:FF44 IO_LY +00:FF45 IO_LYC +00:FF46 IO_DMA +00:FF47 IO_BGP +00:FF48 IO_OBP0 +00:FF49 IO_OBP1 +00:FF4A IO_WY +00:FF4B IO_WX +00:FF4C IO_KEY0 +00:FF4D IO_KEY1 +00:FF4F IO_VBK +00:FF50 IO_BANK +00:FF51 IO_HDMA1 +00:FF52 IO_HDMA2 +00:FF53 IO_HDMA3 +00:FF54 IO_HDMA4 +00:FF55 IO_HDMA5 +00:FF56 IO_RP +00:FF68 IO_BGPI +00:FF69 IO_BGPD +00:FF6A IO_OBPI +00:FF6B IO_OBPD +00:FF6C IO_OPRI +00:FF70 IO_SVBK +00:FF72 IO_UNKNOWN2 +00:FF73 IO_UNKNOWN3 +00:FF74 IO_UNKNOWN4 +00:FF75 IO_UNKNOWN5 +00:FF76 IO_PCM_12 +00:FF77 IO_PCM_34 +00:FF7F IO_UNKNOWN8 +00:FFFF IO_IE diff --git a/bsnes/gb/OpenDialog/cocoa.m b/bsnes/gb/OpenDialog/cocoa.m new file mode 100644 index 00000000..76b9606b --- /dev/null +++ b/bsnes/gb/OpenDialog/cocoa.m @@ -0,0 +1,20 @@ +#import +#include "open_dialog.h" + + +char *do_open_rom_dialog(void) +{ + @autoreleasepool { + NSWindow *key = [NSApp keyWindow]; + NSOpenPanel *dialog = [NSOpenPanel openPanel]; + dialog.title = @"Open ROM"; + dialog.allowedFileTypes = @[@"gb", @"gbc", @"sgb", @"isx"]; + [dialog runModal]; + [key makeKeyAndOrderFront:nil]; + NSString *ret = [[[dialog URLs] firstObject] path]; + if (ret) { + return strdup(ret.UTF8String); + } + return NULL; + } +} diff --git a/bsnes/gb/OpenDialog/gtk.c b/bsnes/gb/OpenDialog/gtk.c new file mode 100644 index 00000000..5b1caa3d --- /dev/null +++ b/bsnes/gb/OpenDialog/gtk.c @@ -0,0 +1,113 @@ +#include "open_dialog.h" +#include +#include +#include +#include +#include + +#define GTK_FILE_CHOOSER_ACTION_OPEN 0 +#define GTK_RESPONSE_ACCEPT -3 +#define GTK_RESPONSE_CANCEL -6 + + +void *_gtk_file_chooser_dialog_new (const char *title, + void *parent, + int action, + const char *first_button_text, + ...); +bool _gtk_init_check (int *argc, char ***argv); +int _gtk_dialog_run(void *); +void _g_free(void *); +void _gtk_widget_destroy(void *); +char *_gtk_file_chooser_get_filename(void *); +void _g_log_set_default_handler (void *function, void *data); +void *_gtk_file_filter_new(void); +void _gtk_file_filter_add_pattern(void *filter, const char *pattern); +void _gtk_file_filter_set_name(void *filter, const char *name); +void _gtk_file_chooser_add_filter(void *dialog, void *filter); +void _gtk_main_iteration(void); +bool _gtk_events_pending(void); + + +#define LAZY(symbol) static typeof(_##symbol) *symbol = NULL;\ +if (symbol == NULL) symbol = dlsym(handle, #symbol);\ +if (symbol == NULL) goto lazy_error +#define TRY_DLOPEN(name) handle = handle? handle : dlopen(name, RTLD_NOW) + +void nop(){} + +char *do_open_rom_dialog(void) +{ + static void *handle = NULL; + + TRY_DLOPEN("libgtk-3.so"); + TRY_DLOPEN("libgtk-3.so.0"); + TRY_DLOPEN("libgtk-2.so"); + TRY_DLOPEN("libgtk-2.so.0"); + + if (!handle) { + goto lazy_error; + } + + + LAZY(gtk_init_check); + LAZY(gtk_file_chooser_dialog_new); + LAZY(gtk_dialog_run); + LAZY(g_free); + LAZY(gtk_widget_destroy); + LAZY(gtk_file_chooser_get_filename); + LAZY(g_log_set_default_handler); + LAZY(gtk_file_filter_new); + LAZY(gtk_file_filter_add_pattern); + LAZY(gtk_file_filter_set_name); + LAZY(gtk_file_chooser_add_filter); + LAZY(gtk_events_pending); + LAZY(gtk_main_iteration); + + /* Shut up GTK */ + g_log_set_default_handler(nop, NULL); + + gtk_init_check(0, 0); + + + void *dialog = gtk_file_chooser_dialog_new("Open ROM", + 0, + GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Open", GTK_RESPONSE_ACCEPT, + NULL ); + + + void *filter = gtk_file_filter_new(); + gtk_file_filter_add_pattern(filter, "*.gb"); + gtk_file_filter_add_pattern(filter, "*.gbc"); + gtk_file_filter_add_pattern(filter, "*.sgb"); + gtk_file_filter_add_pattern(filter, "*.isx"); + gtk_file_filter_set_name(filter, "Game Boy ROMs"); + gtk_file_chooser_add_filter(dialog, filter); + + int res = gtk_dialog_run (dialog); + char *ret = NULL; + + if (res == GTK_RESPONSE_ACCEPT) { + char *filename; + filename = gtk_file_chooser_get_filename(dialog); + ret = strdup(filename); + g_free(filename); + } + + while (gtk_events_pending()) { + gtk_main_iteration(); + } + + gtk_widget_destroy(dialog); + + while (gtk_events_pending()) { + gtk_main_iteration(); + } + return ret; + +lazy_error: + fprintf(stderr, "Failed to display GTK dialog\n"); + return NULL; +} diff --git a/bsnes/gb/OpenDialog/open_dialog.h b/bsnes/gb/OpenDialog/open_dialog.h new file mode 100644 index 00000000..85e5721f --- /dev/null +++ b/bsnes/gb/OpenDialog/open_dialog.h @@ -0,0 +1,6 @@ +#ifndef open_rom_h +#define open_rom_h + +char *do_open_rom_dialog(void); + +#endif /* open_rom_h */ diff --git a/bsnes/gb/OpenDialog/windows.c b/bsnes/gb/OpenDialog/windows.c new file mode 100644 index 00000000..52e281da --- /dev/null +++ b/bsnes/gb/OpenDialog/windows.c @@ -0,0 +1,27 @@ +#include +#include "open_dialog.h" + +char *do_open_rom_dialog(void) +{ + OPENFILENAMEW dialog; + wchar_t filename[MAX_PATH] = {0}; + + memset(&dialog, 0, sizeof(dialog)); + dialog.lStructSize = sizeof(dialog); + dialog.lpstrFile = filename; + dialog.nMaxFile = sizeof(filename); + dialog.lpstrFilter = L"Game Boy ROMs\0*.gb;*.gbc;*.sgb;*.isx\0All files\0*.*\0\0"; + dialog.nFilterIndex = 1; + dialog.lpstrFileTitle = NULL; + dialog.nMaxFileTitle = 0; + dialog.lpstrInitialDir = NULL; + dialog.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; + + if (GetOpenFileNameW(&dialog) == TRUE) { + char *ret = malloc(MAX_PATH * 4); + WideCharToMultiByte(CP_UTF8, 0, filename, sizeof(filename), ret, MAX_PATH * 4, NULL, NULL); + return ret; + } + + return NULL; +} diff --git a/bsnes/gb/QuickLook/CartridgeTemplate.png b/bsnes/gb/QuickLook/CartridgeTemplate.png new file mode 100644 index 00000000..5bf1a2fb Binary files /dev/null and b/bsnes/gb/QuickLook/CartridgeTemplate.png differ diff --git a/bsnes/gb/QuickLook/ColorCartridgeTemplate.png b/bsnes/gb/QuickLook/ColorCartridgeTemplate.png new file mode 100644 index 00000000..5eac5622 Binary files /dev/null and b/bsnes/gb/QuickLook/ColorCartridgeTemplate.png differ diff --git a/bsnes/gb/QuickLook/Info.plist b/bsnes/gb/QuickLook/Info.plist new file mode 100644 index 00000000..b01aae1c --- /dev/null +++ b/bsnes/gb/QuickLook/Info.plist @@ -0,0 +1,63 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDocumentTypes + + + CFBundleTypeRole + QLGenerator + LSItemContentTypes + + com.github.liji32.sameboy.gb + com.github.liji32.sameboy.gbc + com.github.liji32.sameboy.isx + + + + CFBundleExecutable + SameBoyQL + CFBundleIdentifier + com.github.liji32.sameboy.previewer + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SameBoy + CFBundleShortVersionString + Version @VERSION + CFBundleSignature + ???? + CFPlugInDynamicRegisterFunction + + CFPlugInDynamicRegistration + NO + CFPlugInFactories + + 48BC750C-2BB9-49B1-AE80-786E22B3DEB4 + QuickLookGeneratorPluginFactory + + CFPlugInTypes + + 5E2D9680-5022-40FA-B806-43349622E5B9 + + 48BC750C-2BB9-49B1-AE80-786E22B3DEB4 + + + CFPlugInUnloadFunction + + NSHumanReadableCopyright + Copyright © 2015-2020 Lior Halphon + QLNeedsToBeRunInMainThread + + QLPreviewHeight + 144 + QLPreviewWidth + 160 + QLSupportsConcurrentRequests + + QLThumbnailMinimumSize + 64 + + diff --git a/bsnes/gb/QuickLook/UniversalCartridgeTemplate.png b/bsnes/gb/QuickLook/UniversalCartridgeTemplate.png new file mode 100644 index 00000000..1bf4903a Binary files /dev/null and b/bsnes/gb/QuickLook/UniversalCartridgeTemplate.png differ diff --git a/bsnes/gb/QuickLook/exports.sym b/bsnes/gb/QuickLook/exports.sym new file mode 100644 index 00000000..f9796877 --- /dev/null +++ b/bsnes/gb/QuickLook/exports.sym @@ -0,0 +1,3 @@ +_DeallocQuickLookGeneratorPluginType +_QuickLookGeneratorQueryInterface +_QuickLookGeneratorPluginFactory diff --git a/bsnes/gb/QuickLook/generator.m b/bsnes/gb/QuickLook/generator.m new file mode 100644 index 00000000..92bb6ac1 --- /dev/null +++ b/bsnes/gb/QuickLook/generator.m @@ -0,0 +1,119 @@ +#include +#include +#include "get_image_for_rom.h" + +static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) +{ + /* Load the template NSImages when generating the first thumbnail */ + static NSImage *template = nil; + static NSImage *templateUniversal = nil; + static NSImage *templateColor = nil; + static NSBundle *bundle = nil; + static dispatch_once_t onceToken; + if (showBorder) { + dispatch_once(&onceToken, ^{ + bundle = [NSBundle bundleWithIdentifier:@"com.github.liji32.sameboy.previewer"]; + template = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CartridgeTemplate" ofType:@"png"]]; + templateUniversal = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"UniversalCartridgeTemplate" ofType:@"png"]]; + templateColor = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"ColorCartridgeTemplate" ofType:@"png"]]; + }); + } + uint32_t bitmap[160*144]; + uint8_t cgbFlag = 0; + + /* The cgb_boot_fast boot ROM skips the boot animation */ + if (get_image_for_rom([[(__bridge NSURL *)url path] UTF8String], + [[bundle pathForResource:@"cgb_boot_fast" ofType:@"bin"] UTF8String], + bitmap, &cgbFlag)) { + return -1; + } + + /* Convert the screenshot to a CGImageRef */ + CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, bitmap, sizeof(bitmap), NULL); + CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; + CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; + + CGImageRef iref = CGImageCreate(160, + 144, + 8, + 32, + 4 * 160, + colorSpaceRef, + bitmapInfo, + provider, + NULL, + YES, + renderingIntent); + CGContextSetInterpolationQuality(cgContext, kCGInterpolationNone); + NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithGraphicsPort:(void *)cgContext flipped:NO]; + [NSGraphicsContext setCurrentContext:context]; + + + /* Convert the screenshot to a magnified NSImage */ + NSImage *screenshot = [[NSImage alloc] initWithCGImage:iref size:NSMakeSize(160, 144)]; + /* Draw the screenshot */ + if (showBorder) { + [screenshot drawInRect:NSMakeRect(192, 150, 640, 576)]; + } + else { + [screenshot drawInRect:NSMakeRect(0, 0, 640, 576)]; + } + + if (showBorder) { + /* Use the CGB flag to determine the cartridge "look": + - DMG cartridges are grey + - CGB cartridges are transparent + - CGB cartridges that support DMG systems are black + */ + NSImage *effectiveTemplate = nil; + switch (cgbFlag) { + case 0xC0: + effectiveTemplate = templateColor; + break; + case 0x80: + effectiveTemplate = templateUniversal; + break; + default: + effectiveTemplate = template; + } + + /* Mask it with the template (The middle part of the template image is transparent) */ + [effectiveTemplate drawInRect:(NSRect){{0, 0}, template.size}]; + } + + CGColorSpaceRelease(colorSpaceRef); + CGDataProviderRelease(provider); + CGImageRelease(iref); + + return noErr; +} + +OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options) +{ + @autoreleasepool { + CGContextRef cgContext = QLPreviewRequestCreateContext(preview, ((NSSize){640, 576}), true, nil); + if (render(cgContext, url, false) == noErr) { + QLPreviewRequestFlushContext(preview, cgContext); + CGContextRelease(cgContext); + return noErr; + } + CGContextRelease(cgContext); + return -1; + } +} + +OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize) +{ + extern NSString *kQLThumbnailPropertyIconFlavorKey; + @autoreleasepool { + CGContextRef cgContext = QLThumbnailRequestCreateContext(thumbnail, ((NSSize){1024, 1024}), true, (__bridge CFDictionaryRef)(@{kQLThumbnailPropertyIconFlavorKey : @(0)})); + if (render(cgContext, url, true) == noErr) { + QLThumbnailRequestFlushContext(thumbnail, cgContext); + CGContextRelease(cgContext); + return noErr; + } + CGContextRelease(cgContext); + return -1; + } +} diff --git a/bsnes/gb/QuickLook/get_image_for_rom.c b/bsnes/gb/QuickLook/get_image_for_rom.c new file mode 100755 index 00000000..b9f87edb --- /dev/null +++ b/bsnes/gb/QuickLook/get_image_for_rom.c @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include +#include + +#include "get_image_for_rom.h" + +#define LENGTH 60 * 10 + +struct local_data { + unsigned long frames; + bool running; +}; + +static char *async_input_callback(GB_gameboy_t *gb) +{ + return NULL; +} + +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + +} + + +static void vblank(GB_gameboy_t *gb) +{ + + struct local_data *local_data = (struct local_data *)GB_get_user_data(gb); + + if (local_data->frames == LENGTH) { + local_data->running = false; + } + else if (local_data->frames == LENGTH - 1) { + GB_set_rendering_disabled(gb, false); + } + + local_data->frames++; +} + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return (b << 16) | (g << 8) | (r) | 0xFF000000; +} + +int get_image_for_rom(const char *filename, const char *boot_path, uint32_t *output, uint8_t *cgb_flag) +{ + GB_gameboy_t gb; + GB_init(&gb, GB_MODEL_CGB_E); + if (GB_load_boot_rom(&gb, boot_path)) { + GB_free(&gb); + return 1; + } + + GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); + GB_set_pixels_output(&gb, output); + GB_set_rgb_encode_callback(&gb, rgb_encode); + GB_set_async_input_callback(&gb, async_input_callback); + GB_set_log_callback(&gb, log_callback); + GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_EMULATE_HARDWARE); + + size_t length = strlen(filename); + char extension[4] = {0,}; + if (length > 4) { + if (filename[length - 4] == '.') { + extension[0] = tolower(filename[length - 3]); + extension[1] = tolower(filename[length - 2]); + extension[2] = tolower(filename[length - 1]); + } + } + if (strcmp(extension, "isx") == 0) { + if (GB_load_isx(&gb, filename)) { + GB_free(&gb); + return 1; + } + } + else if (GB_load_rom(&gb, filename)) { + GB_free(&gb); + return 1; + } + + /* Run emulation */ + struct local_data local_data = {0,}; + GB_set_user_data(&gb, &local_data); + local_data.running = true; + local_data.frames = 0; + GB_set_rendering_disabled(&gb, true); + GB_set_turbo_mode(&gb, true, true); + + *cgb_flag = GB_read_memory(&gb, 0x143) & 0xC0; + + + while (local_data.running) { + GB_run(&gb); + } + + + GB_free(&gb); + return 0; +} + diff --git a/bsnes/gb/QuickLook/get_image_for_rom.h b/bsnes/gb/QuickLook/get_image_for_rom.h new file mode 100644 index 00000000..598486a5 --- /dev/null +++ b/bsnes/gb/QuickLook/get_image_for_rom.h @@ -0,0 +1,10 @@ +#ifndef get_image_for_rom_h +#define get_image_for_rom_h +#include + +typedef bool (*cancel_callback_t)(void*); + +int get_image_for_rom(const char *filename, const char *boot_path, uint32_t *output, uint8_t *cgb_flag); + + +#endif diff --git a/bsnes/gb/QuickLook/main.c b/bsnes/gb/QuickLook/main.c new file mode 100644 index 00000000..1d1676ac --- /dev/null +++ b/bsnes/gb/QuickLook/main.c @@ -0,0 +1,201 @@ +/* This is an Apple-provided "bootstrap" code for Quick Look generators, nothing intersting here. */ + +#include +#include +#include +#include + +// ----------------------------------------------------------------------------- +// constants +// ----------------------------------------------------------------------------- + +// Don't modify this line +#define PLUGIN_ID "48BC750C-2BB9-49B1-AE80-786E22B3DEB4" + +// +// Below is the generic glue code for all plug-ins. +// +// You should not have to modify this code aside from changing +// names if you decide to change the names defined in the Info.plist +// + + +// ----------------------------------------------------------------------------- +// typedefs +// ----------------------------------------------------------------------------- + +OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize); +OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options); + +// The layout for an instance of QuickLookGeneratorPlugIn +typedef struct __QuickLookGeneratorPluginType +{ + void *conduitInterface; + CFUUIDRef factoryID; + UInt32 refCount; +} QuickLookGeneratorPluginType; + +// ----------------------------------------------------------------------------- +// prototypes +// ----------------------------------------------------------------------------- +// Forward declaration for the IUnknown implementation. +// + +QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFactoryID); +void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInstance); +HRESULT QuickLookGeneratorQueryInterface(void *thisInstance, REFIID iid, LPVOID *ppv); +void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID); +ULONG QuickLookGeneratorPluginAddRef(void *thisInstance); +ULONG QuickLookGeneratorPluginRelease(void *thisInstance); + +// ----------------------------------------------------------------------------- +// myInterfaceFtbl definition +// ----------------------------------------------------------------------------- +// The QLGeneratorInterfaceStruct function table. +// +static QLGeneratorInterfaceStruct myInterfaceFtbl = { + NULL, + QuickLookGeneratorQueryInterface, + QuickLookGeneratorPluginAddRef, + QuickLookGeneratorPluginRelease, + NULL, + NULL, + NULL, + NULL +}; + + +// ----------------------------------------------------------------------------- +// AllocQuickLookGeneratorPluginType +// ----------------------------------------------------------------------------- +// Utility function that allocates a new instance. +// You can do some initial setup for the generator here if you wish +// like allocating globals etc... +// +QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFactoryID) +{ + QuickLookGeneratorPluginType *theNewInstance; + + theNewInstance = (QuickLookGeneratorPluginType *)malloc(sizeof(QuickLookGeneratorPluginType)); + memset(theNewInstance, 0, sizeof(QuickLookGeneratorPluginType)); + + /* Point to the function table Malloc enough to store the stuff and copy the filler from myInterfaceFtbl over */ + theNewInstance->conduitInterface = malloc(sizeof(QLGeneratorInterfaceStruct)); + memcpy(theNewInstance->conduitInterface,&myInterfaceFtbl, sizeof(QLGeneratorInterfaceStruct)); + + /* Retain and keep an open instance refcount for each factory. */ + theNewInstance->factoryID = CFRetain(inFactoryID); + CFPlugInAddInstanceForFactory(inFactoryID); + + /* This function returns the IUnknown interface so set the refCount to one. */ + theNewInstance->refCount = 1; + return theNewInstance; +} + +// ----------------------------------------------------------------------------- +// DeallocQuickLookGeneratorPluginType +// ----------------------------------------------------------------------------- +// Utility function that deallocates the instance when +// the refCount goes to zero. +// In the current implementation generator interfaces are never deallocated +// but implement this as this might change in the future +// +void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInstance) +{ + CFUUIDRef theFactoryID; + + theFactoryID = thisInstance->factoryID; + /* Free the conduitInterface table up */ + free(thisInstance->conduitInterface); + + /* Free the instance structure */ + free(thisInstance); + if (theFactoryID) { + CFPlugInRemoveInstanceForFactory(theFactoryID); + CFRelease(theFactoryID); + } +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorQueryInterface +// ----------------------------------------------------------------------------- +// Implementation of the IUnknown QueryInterface function. +// +HRESULT QuickLookGeneratorQueryInterface(void *thisInstance, REFIID iid, LPVOID *ppv) +{ + CFUUIDRef interfaceID; + + interfaceID = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, iid); + + if (CFEqual(interfaceID, kQLGeneratorCallbacksInterfaceID)) { + /* If the Right interface was requested, bump the ref count, + * set the ppv parameter equal to the instance, and + * return good status. + */ + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType *)thisInstance)->conduitInterface)->GenerateThumbnailForURL = GenerateThumbnailForURL; + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType *)thisInstance)->conduitInterface)->GeneratePreviewForURL = GeneratePreviewForURL; + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType*)thisInstance)->conduitInterface)->AddRef(thisInstance); + *ppv = thisInstance; + CFRelease(interfaceID); + return S_OK; + } + else { + /* Requested interface unknown, bail with error. */ + *ppv = NULL; + CFRelease(interfaceID); + return E_NOINTERFACE; + } +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorPluginAddRef +// ----------------------------------------------------------------------------- +// Implementation of reference counting for this type. Whenever an interface +// is requested, bump the refCount for the instance. NOTE: returning the +// refcount is a convention but is not required so don't rely on it. +// +ULONG QuickLookGeneratorPluginAddRef(void *thisInstance) +{ + ((QuickLookGeneratorPluginType *)thisInstance )->refCount += 1; + return ((QuickLookGeneratorPluginType*) thisInstance)->refCount; +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorPluginRelease +// ----------------------------------------------------------------------------- +// When an interface is released, decrement the refCount. +// If the refCount goes to zero, deallocate the instance. +// +ULONG QuickLookGeneratorPluginRelease(void *thisInstance) +{ + ((QuickLookGeneratorPluginType*)thisInstance)->refCount -= 1; + if (((QuickLookGeneratorPluginType*)thisInstance)->refCount == 0) { + DeallocQuickLookGeneratorPluginType((QuickLookGeneratorPluginType*)thisInstance ); + return 0; + } + else { + return ((QuickLookGeneratorPluginType*) thisInstance )->refCount; + } +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorPluginFactory +// ----------------------------------------------------------------------------- +void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID) +{ + QuickLookGeneratorPluginType *result; + CFUUIDRef uuid; + + /* If correct type is being requested, allocate an + * instance of kQLGeneratorTypeID and return the IUnknown interface. + */ + if (CFEqual(typeID, kQLGeneratorTypeID)) { + uuid = CFUUIDCreateFromString(kCFAllocatorDefault, CFSTR(PLUGIN_ID)); + result = AllocQuickLookGeneratorPluginType(uuid); + CFRelease(uuid); + return result; + } + /* If the requested type is incorrect, return NULL. */ + return NULL; +} + diff --git a/bsnes/gb/README.md b/bsnes/gb/README.md new file mode 100644 index 00000000..1107fdc4 --- /dev/null +++ b/bsnes/gb/README.md @@ -0,0 +1,56 @@ +# SameBoy + +SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator, written in portable C. It has a native Cocoa frontend for macOS, an SDL frontend for other operating systems, and a libretro core. It also includes a text-based debugger with an expression evaluator. Visit [the website](https://sameboy.github.io/). + +## Features +Features common to both Cocoa and SDL versions: + * Supports Game Boy (DMG) and Game Boy Color (CGB) emulation + * Lets you choose the model you want to emulate regardless of ROM + * High quality 96KHz audio + * Battery save support + * Save states + * Includes open source DMG and CGB boot ROMs: + * Complete support for (and documentation of) *all* game-specific palettes in the CGB boot ROM, for accurate emulation of Game Boy games on a Game Boy Color + * Supports manual palette selection with key combinations, with 4 additional new palettes (A + B + direction) + * Supports palette selection in a CGB game, forcing it to run in 'paletted' DMG mode, if ROM allows doing so. + * Support for games with a non-Nintendo logo in the header + * No long animation in the DMG boot + * Advanced text-based debugger with an expression evaluator, disassembler, conditional breakpoints, conditional watchpoints, backtracing and other features + * Extremely high accuracy + * Emulates [PCM_12 and PCM_34 registers](https://github.com/LIJI32/GBVisualizer) + * T-cycle accurate emulation of LCD timing effects, supporting the Demotronic trick, Prehistorik Man, [GBVideoPlayer](https://github.com/LIJI32/GBVideoPlayer) and other tech demos + * Real time clock emulation + * Retina/High DPI display support, allowing a wider range of scaling factors without artifacts + * Optional frame blending (Requires OpenGL 3.2 or later) + * Several [scaling algorithms](https://sameboy.github.io/scaling/) (Including exclusive algorithms like OmniScale and Anti-aliased Scale2x; Requires OpenGL 3.2 or later or Metal) + +Features currently supported only with the Cocoa version: + * Native Cocoa interface, with support for all system-wide features, such as drag-and-drop and smart titlebars + * Game Boy Camera support + +[Read more](https://sameboy.github.io/features/). + +## Compatibility +SameBoy passes all of [blargg's test ROMs](http://gbdev.gg8.se/wiki/articles/Test_ROMs#Blargg.27s_tests), all of [mooneye-gb's](https://github.com/Gekkio/mooneye-gb) tests (Some tests require the original boot ROMs), and all of [Wilbert Pol's tests](https://github.com/wilbertpol/mooneye-gb/tree/master/tests/acceptance). SameBoy should work with most games and demos, please [report](https://github.com/LIJI32/SameBoy/issues/new) any broken ROM. The latest results for SameBoy's automatic tester are available [here](https://sameboy.github.io/automation/). + +## Contributing +SameBoy is an open-source project licensed under the MIT license, and you're welcome to contribute by creating issues, implementing new features, improving emulation accuracy and fixing existing open issues. You can read the [contribution guidelines](CONTRIBUTING.md) to make sure your contributions are as effective as possible. + +## Compilation +SameBoy requires the following tools and libraries to build: + * clang (Recommended; required for macOS) or GCC + * make + * macOS Cocoa port: macOS SDK and Xcode (For command line tools and ibtool) + * SDL port: libsdl2 + * [rgbds](https://github.com/bentley/rgbds/releases/), for boot ROM compilation + +On Windows, SameBoy also requires: + * Visual Studio (For headers, etc.) + * [GnuWin](http://gnuwin32.sourceforge.net/) + * Running vcvars32 before running make. Make sure all required tools and libraries are in %PATH% and %lib%, respectively. (see [Build FAQ](https://github.com/LIJI32/SameBoy/blob/master/build-faq.md) for more details on Windows compilation) + +To compile, simply run `make`. The targets are `cocoa` (Default for macOS), `sdl` (Default for everything else), `libretro`, `bootroms` and `tester`. You may also specify `CONF=debug` (default), `CONF=release`, `CONF=native_release` or `CONF=fat_release` to control optimization, symbols and multi-architectures. `native_release` is faster than `release`, but is optimized to the host's CPU and therefore is not portable. `fat_release` is exclusive to macOS and builds x86-64 and ARM64 fat binaries; this requires using a recent enough `clang` and macOS SDK using `xcode-select`, or setting them explicitly with `CC=` and `SYSROOT=`, respectively. All other configurations will build to your host architecture. You may set `BOOTROMS_DIR=...` to a directory containing precompiled boot ROM files, otherwise the build system will compile and use SameBoy's own boot ROMs. + +By default, the SDL port will look for resource files with a path relative to executable. If you are packaging SameBoy, you may wish to override this by setting the `DATA_DIR` variable during compilation to the target path of the directory containing all files (apart from the executable, that's not necessary) from the `build/bin/SDL` directory in the source tree. Make sure the variable ends with a `/` character. + +SameBoy was compiled and tested on macOS, Ubuntu and 64-bit Windows 7. diff --git a/bsnes/gb/SDL/audio/audio.h b/bsnes/gb/SDL/audio/audio.h new file mode 100644 index 00000000..acaa011d --- /dev/null +++ b/bsnes/gb/SDL/audio/audio.h @@ -0,0 +1,16 @@ +#ifndef sdl_audio_h +#define sdl_audio_h + +#include +#include +#include + +bool GB_audio_is_playing(void); +void GB_audio_set_paused(bool paused); +void GB_audio_clear_queue(void); +unsigned GB_audio_get_frequency(void); +size_t GB_audio_get_queue_length(void); +void GB_audio_queue_sample(GB_sample_t *sample); +void GB_audio_init(void); + +#endif /* sdl_audio_h */ diff --git a/bsnes/gb/SDL/audio/sdl.c b/bsnes/gb/SDL/audio/sdl.c new file mode 100644 index 00000000..12ee69ae --- /dev/null +++ b/bsnes/gb/SDL/audio/sdl.c @@ -0,0 +1,96 @@ +#include "audio.h" +#include + +#ifndef _WIN32 +#define AUDIO_FREQUENCY 96000 +#include +#else +#include +/* Windows (well, at least my VM) can't handle 96KHz sound well :( */ + +/* felsqualle says: For SDL 2.0.6+ using the WASAPI driver, the highest freq. + we can get is 48000. 96000 also works, but always has some faint crackling in + the audio, no matter how high or low I set the buffer length... + Not quite satisfied with that solution, because acc. to SDL2 docs, + 96k + WASAPI *should* work. */ + +#define AUDIO_FREQUENCY 48000 +#endif + +/* Compatibility with older SDL versions */ +#ifndef SDL_AUDIO_ALLOW_SAMPLES_CHANGE +#define SDL_AUDIO_ALLOW_SAMPLES_CHANGE 0 +#endif + +static SDL_AudioDeviceID device_id; +static SDL_AudioSpec want_aspec, have_aspec; + +#define AUDIO_BUFFER_SIZE 512 +static unsigned buffer_pos = 0; +static GB_sample_t audio_buffer[AUDIO_BUFFER_SIZE]; + +bool GB_audio_is_playing(void) +{ + return SDL_GetAudioDeviceStatus(device_id) == SDL_AUDIO_PLAYING; +} + +void GB_audio_set_paused(bool paused) +{ + GB_audio_clear_queue(); + SDL_PauseAudioDevice(device_id, paused); +} + +void GB_audio_clear_queue(void) +{ + SDL_ClearQueuedAudio(device_id); +} + +unsigned GB_audio_get_frequency(void) +{ + return have_aspec.freq; +} + +size_t GB_audio_get_queue_length(void) +{ + return SDL_GetQueuedAudioSize(device_id); +} + +void GB_audio_queue_sample(GB_sample_t *sample) +{ + audio_buffer[buffer_pos++] = *sample; + + if (buffer_pos == AUDIO_BUFFER_SIZE) { + buffer_pos = 0; + SDL_QueueAudio(device_id, (const void *)audio_buffer, sizeof(audio_buffer)); + } +} + +void GB_audio_init(void) +{ + /* Configure Audio */ + memset(&want_aspec, 0, sizeof(want_aspec)); + want_aspec.freq = AUDIO_FREQUENCY; + want_aspec.format = AUDIO_S16SYS; + want_aspec.channels = 2; + want_aspec.samples = 512; + + SDL_version _sdl_version; + SDL_GetVersion(&_sdl_version); + unsigned sdl_version = _sdl_version.major * 1000 + _sdl_version.minor * 100 + _sdl_version.patch; + +#ifndef _WIN32 + /* SDL 2.0.5 on macOS and Linux introduced a bug where certain combinations of buffer lengths and frequencies + fail to produce audio correctly. */ + if (sdl_version >= 2005) { + want_aspec.samples = 2048; + } +#else + if (sdl_version < 2006) { + /* Since WASAPI audio was introduced in SDL 2.0.6, we have to lower the audio frequency + to 44100 because otherwise we would get garbled audio output.*/ + want_aspec.freq = 44100; + } +#endif + + device_id = SDL_OpenAudioDevice(0, 0, &want_aspec, &have_aspec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_SAMPLES_CHANGE); +} diff --git a/bsnes/gb/SDL/background.bmp b/bsnes/gb/SDL/background.bmp new file mode 100644 index 00000000..0f6192d6 Binary files /dev/null and b/bsnes/gb/SDL/background.bmp differ diff --git a/bsnes/gb/SDL/font.c b/bsnes/gb/SDL/font.c new file mode 100644 index 00000000..93f3fa94 --- /dev/null +++ b/bsnes/gb/SDL/font.c @@ -0,0 +1,1038 @@ +#include "font.h" + +#define _ 0 +#define X 1 + +uint8_t font[] = { + /* */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* ! */ + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* " */ + X, X, _, X, X, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* # */ + _, X, _, X, _, _, + _, X, _, X, _, _, + X, X, X, X, X, _, + _, X, _, X, _, _, + X, X, X, X, X, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + _, _, _, _, _, _, + + /* $ */ + _, _, X, _, _, _, + _, X, X, X, _, _, + X, _, X, _, X, _, + _, X, X, _, _, _, + _, _, X, X, _, _, + X, _, X, _, X, _, + _, X, X, X, _, _, + _, _, X, _, _, _, + + /* % */ + _, _, _, _, _, _, + X, X, _, _, _, X, + X, X, _, _, X, _, + _, _, _, X, _, _, + _, _, X, _, _, _, + _, X, _, _, X, X, + X, _, _, _, X, X, + _, _, _, _, _, _, + + /* & */ + _, X, X, _, _, _, + X, _, _, X, _, _, + X, _, _, X, _, _, + _, X, X, _, X, _, + X, _, _, X, _, _, + X, _, _, X, _, _, + _, X, X, _, X, _, + _, _, _, _, _, _, + + /* ' */ + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* ( */ + _, _, _, X, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, _, X, _, _, _, + _, _, _, X, _, _, + + /* ) */ + _, X, _, _, _, _, + _, _, X, _, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + + /* * */ + _, _, _, _, _, _, + _, _, X, _, _, _, + X, _, X, _, X, _, + _, X, X, X, _, _, + X, _, X, _, X, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* + */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + X, X, X, X, X, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /*, */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + + /* - */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* . */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* / */ + _, _, _, _, X, _, + _, _, _, _, X, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + + /* 0 */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* 1 */ + _, _, X, _, _, _, + X, X, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + /* 2 */ + _, X, X, X, _, _, + X, _, _, _, X, _, + _, _, _, _, X, _, + _, _, X, X, _, _, + _, X, _, _, _, _, + X, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + /* 3 */ + _, X, X, X, _, _, + X, _, _, _, X, _, + _, _, _, _, X, _, + _, _, X, X, _, _, + _, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* 4 */ + _, _, _, X, _, _, + _, _, X, X, _, _, + _, X, _, X, _, _, + X, _, _, X, _, _, + X, X, X, X, X, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, _, _, _, + + /* 5 */ + X, X, X, X, X, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, X, X, X, _, _, + _, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* 6 */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, _, _, + X, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* 7 */ + X, X, X, X, X, _, + _, _, _, _, X, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* 8 */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* 9 */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, X, _, + _, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* : */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* ; */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + + /* < */ + _, _, _, _, _, _, + _, _, _, _, X, _, + _, _, X, X, _, _, + X, X, _, _, _, _, + X, X, _, _, _, _, + _, _, X, X, _, _, + _, _, _, _, X, _, + _, _, _, _, _, _, + + /* = */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* > */ + _, _, _, _, _, _, + X, _, _, _, _, _, + _, X, X, _, _, _, + _, _, _, X, X, _, + _, _, _, X, X, _, + _, X, X, _, _, _, + X, _, _, _, _, _, + _, _, _, _, _, _, + + /* ? */ + _, X, X, X, _, _, + X, _, _, _, X, _, + _, _, _, _, X, _, + _, _, X, X, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* @ */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, X, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + X, _, _, X, X, _, + X, _, _, _, _, _, + _, X, X, X, X, _, + + /* A */ + _, _, X, _, _, _, + _, _, X, _, _, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* B */ + X, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, _, _, + _, _, _, _, _, _, + + /* C */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* D */ + X, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, _, _, + _, _, _, _, _, _, + + /* E */ + X, X, X, X, X, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, X, X, X, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + /* F */ + X, X, X, X, X, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, X, X, X, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + _, _, _, _, _, _, + + /* G */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, _, _, + X, _, X, X, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* H */ + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* I */ + X, X, X, X, X, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + /* J */ + _, _, X, X, X, _, + _, _, _, _, X, _, + _, _, _, _, X, _, + _, _, _, _, X, _, + _, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* K */ + X, _, _, _, X, _, + X, _, _, X, _, _, + X, _, X, _, _, _, + X, X, _, _, _, _, + X, _, X, _, _, _, + X, _, _, X, _, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* L */ + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + /* M */ + X, _, _, _, X, _, + X, X, _, X, X, _, + X, X, _, X, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* N */ + X, _, _, _, X, _, + X, X, _, _, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + X, _, _, X, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* O */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* P */ + X, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + _, _, _, _, _, _, + + /* Q */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, X, _, X, _, + _, X, X, X, _, _, + _, _, _, _, X, X, + + /* R */ + X, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, _, _, + X, _, X, _, _, _, + X, _, _, X, _, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* S */ + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, _, _, + _, X, X, X, _, _, + _, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* T */ + X, X, X, X, X, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* U */ + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* V */ + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* W */ + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + X, X, _, X, X, _, + X, X, _, X, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* X */ + X, _, _, _, X, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + _, _, X, _, _, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* Y */ + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, _, X, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* Z */ + X, X, X, X, X, _, + _, _, _, _, X, _, + _, _, _, X, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + X, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + /* [ */ + _, X, X, X, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, X, X, _, _, + + /* \ */ + _, X, _, _, _, _, + _, X, _, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, _, X, _, + _, _, _, _, X, _, + + /* ] */ + _, X, X, X, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, X, X, X, _, _, + + /* ^ */ + _, _, X, _, _, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* _ */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, X, X, X, + + /* ` */ + _, X, _, _, _, _, + _, _, X, _, _, _, + _, _, _, X, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* a */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, X, _, _, _, + _, _, _, X, _, _, + _, X, X, X, _, _, + X, _, _, X, _, _, + _, X, X, _, X, _, + _, _, _, _, _, _, + + /* b */ + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, X, X, _, _, + X, X, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, _, _, + _, _, _, _, _, _, + + /* c */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, _, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* d */ + _, _, _, _, X, _, + _, _, _, _, X, _, + _, X, X, X, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, X, _, + _, _, _, _, _, _, + + /* e */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, X, X, _, _, + X, _, _, _, X, _, + X, X, X, X, X, _, + X, _, _, _, _, _, + _, X, X, X, X, _, + _, _, _, _, _, _, + + /* f */ + _, _, X, X, _, _, + _, X, _, _, _, _, + X, X, X, X, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, _, _, _, _, _, + + /* g */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, X, X, X, _, + X, _, _, X, _, _, + _, X, X, _, _, _, + _, _, _, X, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + + /* h */ + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, X, X, _, _, + X, X, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* i */ + _, _, X, _, _, _, + _, _, _, _, _, _, + X, X, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* j */ + _, _, X, _, _, _, + _, _, _, _, _, _, + X, X, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + X, X, _, _, _, _, + + /* k */ + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, X, _, + X, _, X, X, _, _, + X, X, _, _, _, _, + X, _, X, X, _, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* l */ + X, X, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, X, X, _, + _, _, _, _, _, _, + + /* m */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, X, _, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + _, _, _, _, _, _, + + /* n */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, _, X, X, _, _, + X, X, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* o */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, X, X, _, _, + _, _, _, _, _, _, + + /* p */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, X, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, _, _, X, _, + X, _, X, X, _, _, + X, _, _, _, _, _, + + /* q */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, X, X, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, X, X, _, + _, X, X, _, X, _, + _, _, _, _, X, _, + + /* r */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, _, X, _, _, + _, X, X, _, X, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, _, _, _, _, _, + + /* s */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, X, X, X, X, _, + X, _, _, _, _, _, + _, X, X, X, _, _, + _, _, _, _, X, _, + X, X, X, X, _, _, + _, _, _, _, _, _, + + /* t */ + _, _, _, _, _, _, + _, X, _, _, _, _, + X, X, X, X, X, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, _, X, X, X, _, + _, _, _, _, _, _, + + /* u */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, X, X, _, + _, X, X, _, X, _, + _, _, _, _, _, _, + + /* v */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* w */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, _, _, _, X, _, + X, _, X, _, X, _, + X, _, X, _, X, _, + _, X, _, X, _, _, + _, X, _, X, _, _, + _, _, _, _, _, _, + + /* x */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, _, _, _, X, _, + _, X, _, X, _, _, + _, _, X, _, _, _, + _, X, _, X, _, _, + X, _, _, _, X, _, + _, _, _, _, _, _, + + /* y */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, X, X, _, + _, X, X, _, X, _, + _, _, _, _, X, _, + _, X, X, X, _, _, + + /* z */ + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, X, X, _, + _, _, _, X, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + /* { */ + _, _, X, X, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, X, _, _, _, _, + _, X, _, _, _, _, + _, _, X, X, _, _, + + /* | */ + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + + /* } */ + _, X, X, _, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, X, _, _, + _, _, _, X, _, _, + _, X, X, _, _, _, + + /* ~ */ + _, _, _, _, _, X, + _, _, _, _, _, _, + _, _, X, _, _, X, + _, X, _, X, _, X, + _, X, _, _, X, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* Custom characters */ + /* Selection */ + _, X, _, _, _, _, + _, X, X, _, _, _, + _, X, X, X, _, _, + _, X, X, X, X, _, + _, X, X, X, _, _, + _, X, X, _, _, _, + _, X, _, _, _, _, + _, _, _, _, _, _, + + + /* CTRL symbol */ + _, X, X, _, _, X, + X, _, _, X, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, _, _, _, + X, _, _, X, _, _, + _, X, X, _, _, _, + _, _, _, _, _, _, + + X, X, _, X, X, X, + X, _, _, X, _, _, + X, _, _, X, _, _, + X, _, _, X, _, _, + X, _, _, X, X, X, + X, _, _, X, _, X, + X, _, _, X, _, _, + _, _, _, _, _, _, + + _, _, X, _, _, _, + X, _, X, _, _, _, + X, _, X, _, _, _, + X, _, X, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + X, _, X, X, X, X, + _, _, _, _, _, _, + + /* Shift symbol */ + _, _, X, X, _, _, + _, X, X, X, X, _, + X, X, X, X, X, X, + _, X, X, X, X, _, + _, X, X, X, X, _, + _, X, X, X, X, _, + _, X, X, X, X, _, + _, _, _, _, _, _, + + /* Cmd symbol */ + + _, _, _, _, _, X, + _, _, _, _, X, _, + _, _, _, _, _, X, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, X, + _, _, _, _, X, _, + _, _, _, _, _, X, + + _, _, _, _, X, _, + X, _, _, X, _, X, + X, X, X, X, X, _, + X, _, _, X, _, _, + X, _, _, X, _, _, + X, X, X, X, X, _, + X, _, _, X, _, X, + _, _, _, _, X, _, + + /* Left Arrow */ + _, _, _, _, X, _, + _, _, _, X, X, _, + _, _, X, X, X, _, + _, X, X, X, X, _, + _, _, X, X, X, _, + _, _, _, X, X, _, + _, _, _, _, X, _, + _, _, _, _, _, _, +}; + +const uint8_t font_max = sizeof(font) / GLYPH_HEIGHT / GLYPH_WIDTH + ' '; diff --git a/bsnes/gb/SDL/font.h b/bsnes/gb/SDL/font.h new file mode 100644 index 00000000..21753a8d --- /dev/null +++ b/bsnes/gb/SDL/font.h @@ -0,0 +1,16 @@ +#ifndef font_h +#define font_h + +#include +extern uint8_t font[]; +extern const uint8_t font_max; +#define GLYPH_HEIGHT 8 +#define GLYPH_WIDTH 6 +#define LEFT_ARROW_STRING "\x86" +#define RIGHT_ARROW_STRING "\x7f" +#define SELECTION_STRING RIGHT_ARROW_STRING +#define CTRL_STRING "\x80\x81\x82" +#define SHIFT_STRING "\x83" +#define CMD_STRING "\x84\x85" +#endif /* font_h */ + diff --git a/bsnes/gb/SDL/gui.c b/bsnes/gb/SDL/gui.c new file mode 100644 index 00000000..62656e8d --- /dev/null +++ b/bsnes/gb/SDL/gui.c @@ -0,0 +1,1419 @@ +#include +#include +#include +#include +#include +#include +#include +#include "utils.h" +#include "gui.h" +#include "font.h" + +static const SDL_Color gui_palette[4] = {{8, 24, 16,}, {57, 97, 57,}, {132, 165, 99}, {198, 222, 140}}; +static uint32_t gui_palette_native[4]; + +SDL_Window *window = NULL; +SDL_Renderer *renderer = NULL; +SDL_Texture *texture = NULL; +SDL_PixelFormat *pixel_format = NULL; +enum pending_command pending_command; +unsigned command_parameter; + +#ifdef __APPLE__ +#define MODIFIER_NAME " " CMD_STRING +#else +#define MODIFIER_NAME CTRL_STRING +#endif + +shader_t shader; +static SDL_Rect rect; +static unsigned factor; + +void render_texture(void *pixels, void *previous) +{ + if (renderer) { + if (pixels) { + SDL_UpdateTexture(texture, NULL, pixels, GB_get_screen_width(&gb) * sizeof (uint32_t)); + } + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer, texture, NULL, NULL); + SDL_RenderPresent(renderer); + } + else { + static void *_pixels = NULL; + if (pixels) { + _pixels = pixels; + } + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + GB_frame_blending_mode_t mode = configuration.blending_mode; + if (!previous) { + mode = GB_FRAME_BLENDING_MODE_DISABLED; + } + else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) { + if (GB_is_sgb(&gb)) { + mode = GB_FRAME_BLENDING_MODE_SIMPLE; + } + else { + mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; + } + } + render_bitmap_with_shader(&shader, _pixels, previous, + GB_get_screen_width(&gb), GB_get_screen_height(&gb), + rect.x, rect.y, rect.w, rect.h, + mode); + SDL_GL_SwapWindow(window); + } +} + +configuration_t configuration = +{ + .keys = { + SDL_SCANCODE_RIGHT, + SDL_SCANCODE_LEFT, + SDL_SCANCODE_UP, + SDL_SCANCODE_DOWN, + SDL_SCANCODE_X, + SDL_SCANCODE_Z, + SDL_SCANCODE_BACKSPACE, + SDL_SCANCODE_RETURN, + SDL_SCANCODE_SPACE + }, + .keys_2 = { + SDL_SCANCODE_TAB, + SDL_SCANCODE_LSHIFT, + }, + .joypad_configuration = { + 13, + 14, + 11, + 12, + 0, + 1, + 9, + 8, + 10, + 4, + -1, + 5, + }, + .joypad_axises = { + 0, + 1, + }, + .color_correction_mode = GB_COLOR_CORRECTION_EMULATE_HARDWARE, + .highpass_mode = GB_HIGHPASS_ACCURATE, + .scaling_mode = GB_SDL_SCALING_INTEGER_FACTOR, + .blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE, + .rewind_length = 60 * 2, + .model = MODEL_CGB, + .volume = 100, + .rumble_mode = GB_RUMBLE_ALL_GAMES, + .default_scale = 2, +}; + + +static const char *help[] = { +"Drop a ROM to play.\n" +"\n" +"Keyboard Shortcuts:\n" +" Open Menu: Escape\n" +" Open ROM: " MODIFIER_NAME "+O\n" +" Reset: " MODIFIER_NAME "+R\n" +" Pause: " MODIFIER_NAME "+P\n" +" Save state: " MODIFIER_NAME "+(0-9)\n" +" Load state: " MODIFIER_NAME "+" SHIFT_STRING "+(0-9)\n" +" Toggle Fullscreen " MODIFIER_NAME "+F\n" +#ifdef __APPLE__ +" Mute/Unmute: " MODIFIER_NAME "+" SHIFT_STRING "+M\n" +#else +" Mute/Unmute: " MODIFIER_NAME "+M\n" +#endif +" Break Debugger: " CTRL_STRING "+C" +}; + +void update_viewport(void) +{ + int win_width, win_height; + SDL_GL_GetDrawableSize(window, &win_width, &win_height); + int logical_width, logical_height; + SDL_GetWindowSize(window, &logical_width, &logical_height); + factor = win_width / logical_width; + + double x_factor = win_width / (double) GB_get_screen_width(&gb); + double y_factor = win_height / (double) GB_get_screen_height(&gb); + + if (configuration.scaling_mode == GB_SDL_SCALING_INTEGER_FACTOR) { + x_factor = (unsigned)(x_factor); + y_factor = (unsigned)(y_factor); + } + + if (configuration.scaling_mode != GB_SDL_SCALING_ENTIRE_WINDOW) { + if (x_factor > y_factor) { + x_factor = y_factor; + } + else { + y_factor = x_factor; + } + } + + unsigned new_width = x_factor * GB_get_screen_width(&gb); + unsigned new_height = y_factor * GB_get_screen_height(&gb); + + rect = (SDL_Rect){(win_width - new_width) / 2, (win_height - new_height) /2, + new_width, new_height}; + + if (renderer) { + SDL_RenderSetViewport(renderer, &rect); + } + else { + glViewport(rect.x, rect.y, rect.w, rect.h); + } +} + +static void rescale_window(void) +{ + SDL_SetWindowSize(window, GB_get_screen_width(&gb) * configuration.default_scale, GB_get_screen_height(&gb) * configuration.default_scale); +} + +/* Does NOT check for bounds! */ +static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigned char ch, uint32_t color) +{ + if (ch < ' ' || ch > font_max) { + ch = '?'; + } + + uint8_t *data = &font[(ch - ' ') * GLYPH_WIDTH * GLYPH_HEIGHT]; + + for (unsigned y = GLYPH_HEIGHT; y--;) { + for (unsigned x = GLYPH_WIDTH; x--;) { + if (*(data++)) { + (*buffer) = color; + } + buffer++; + } + buffer += width - GLYPH_WIDTH; + } +} + +static unsigned scroll = 0; +static void draw_unbordered_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, unsigned y, const char *string, uint32_t color) +{ + y -= scroll; + unsigned orig_x = x; + unsigned y_offset = (GB_get_screen_height(&gb) - 144) / 2; + while (*string) { + if (*string == '\n') { + x = orig_x; + y += GLYPH_HEIGHT + 4; + string++; + continue; + } + + if (x > width - GLYPH_WIDTH || y == 0 || y - y_offset > 144 - GLYPH_HEIGHT) { + break; + } + + draw_char(&buffer[x + width * y], width, height, *string, color); + x += GLYPH_WIDTH; + string++; + } +} + +static void draw_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, unsigned y, const char *string, uint32_t color, uint32_t border) +{ + draw_unbordered_text(buffer, width, height, x - 1, y, string, border); + draw_unbordered_text(buffer, width, height, x + 1, y, string, border); + draw_unbordered_text(buffer, width, height, x, y - 1, string, border); + draw_unbordered_text(buffer, width, height, x, y + 1, string, border); + draw_unbordered_text(buffer, width, height, x, y, string, color); +} + +enum decoration { + DECORATION_NONE, + DECORATION_SELECTION, + DECORATION_ARROWS, +}; + +static void draw_text_centered(uint32_t *buffer, unsigned width, unsigned height, unsigned y, const char *string, uint32_t color, uint32_t border, enum decoration decoration) +{ + unsigned x = width / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2; + draw_text(buffer, width, height, x, y, string, color, border); + switch (decoration) { + case DECORATION_SELECTION: + draw_text(buffer, width, height, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border); + break; + case DECORATION_ARROWS: + draw_text(buffer, width, height, x - GLYPH_WIDTH, y, LEFT_ARROW_STRING, color, border); + draw_text(buffer, width, height, width - x, y, RIGHT_ARROW_STRING, color, border); + break; + + case DECORATION_NONE: + break; + } +} + +struct menu_item { + const char *string; + void (*handler)(unsigned); + const char *(*value_getter)(unsigned); + void (*backwards_handler)(unsigned); +}; +static const struct menu_item *current_menu = NULL; +static const struct menu_item *root_menu = NULL; +static unsigned current_selection = 0; + +static enum { + SHOWING_DROP_MESSAGE, + SHOWING_MENU, + SHOWING_HELP, + WAITING_FOR_KEY, + WAITING_FOR_JBUTTON, +} gui_state; + +static unsigned joypad_configuration_progress = 0; +static uint8_t joypad_axis_temp; + +static void item_exit(unsigned index) +{ + pending_command = GB_SDL_QUIT_COMMAND; +} + +static unsigned current_help_page = 0; +static void item_help(unsigned index) +{ + current_help_page = 0; + gui_state = SHOWING_HELP; +} + +static void enter_emulation_menu(unsigned index); +static void enter_graphics_menu(unsigned index); +static void enter_controls_menu(unsigned index); +static void enter_joypad_menu(unsigned index); +static void enter_audio_menu(unsigned index); + +extern void set_filename(const char *new_filename, typeof(free) *new_free_function); +static void open_rom(unsigned index) +{ + char *filename = do_open_rom_dialog(); + if (filename) { + set_filename(filename, free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + } +} + +static const struct menu_item paused_menu[] = { + {"Resume", NULL}, + {"Open ROM", open_rom}, + {"Emulation Options", enter_emulation_menu}, + {"Graphic Options", enter_graphics_menu}, + {"Audio Options", enter_audio_menu}, + {"Keyboard", enter_controls_menu}, + {"Joypad", enter_joypad_menu}, + {"Help", item_help}, + {"Quit SameBoy", item_exit}, + {NULL,} +}; + +static const struct menu_item *const nonpaused_menu = &paused_menu[1]; + +static void return_to_root_menu(unsigned index) +{ + current_menu = root_menu; + current_selection = 0; + scroll = 0; +} + +static void cycle_model(unsigned index) +{ + + configuration.model++; + if (configuration.model == MODEL_MAX) { + configuration.model = 0; + } + pending_command = GB_SDL_RESET_COMMAND; +} + +static void cycle_model_backwards(unsigned index) +{ + if (configuration.model == 0) { + configuration.model = MODEL_MAX; + } + configuration.model--; + pending_command = GB_SDL_RESET_COMMAND; +} + +const char *current_model_string(unsigned index) +{ + return (const char *[]){"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy"} + [configuration.model]; +} + +static void cycle_sgb_revision(unsigned index) +{ + + configuration.sgb_revision++; + if (configuration.sgb_revision == SGB_MAX) { + configuration.sgb_revision = 0; + } + pending_command = GB_SDL_RESET_COMMAND; +} + +static void cycle_sgb_revision_backwards(unsigned index) +{ + if (configuration.sgb_revision == 0) { + configuration.sgb_revision = SGB_MAX; + } + configuration.sgb_revision--; + pending_command = GB_SDL_RESET_COMMAND; +} + +const char *current_sgb_revision_string(unsigned index) +{ + return (const char *[]){"Super Game Boy NTSC", "Super Game Boy PAL", "Super Game Boy 2"} + [configuration.sgb_revision]; +} + +static const uint32_t rewind_lengths[] = {0, 10, 30, 60, 60 * 2, 60 * 5, 60 * 10}; +static const char *rewind_strings[] = {"Disabled", + "10 Seconds", + "30 Seconds", + "1 Minute", + "2 Minutes", + "5 Minutes", + "10 Minutes", +}; + +static void cycle_rewind(unsigned index) +{ + for (unsigned i = 0; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]) - 1; i++) { + if (configuration.rewind_length == rewind_lengths[i]) { + configuration.rewind_length = rewind_lengths[i + 1]; + GB_set_rewind_length(&gb, configuration.rewind_length); + return; + } + } + configuration.rewind_length = rewind_lengths[0]; + GB_set_rewind_length(&gb, configuration.rewind_length); +} + +static void cycle_rewind_backwards(unsigned index) +{ + for (unsigned i = 1; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]); i++) { + if (configuration.rewind_length == rewind_lengths[i]) { + configuration.rewind_length = rewind_lengths[i - 1]; + GB_set_rewind_length(&gb, configuration.rewind_length); + return; + } + } + configuration.rewind_length = rewind_lengths[sizeof(rewind_lengths) / sizeof(rewind_lengths[0]) - 1]; + GB_set_rewind_length(&gb, configuration.rewind_length); +} + +const char *current_rewind_string(unsigned index) +{ + for (unsigned i = 0; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]); i++) { + if (configuration.rewind_length == rewind_lengths[i]) { + return rewind_strings[i]; + } + } + return "Custom"; +} + +static const struct menu_item emulation_menu[] = { + {"Emulated Model:", cycle_model, current_model_string, cycle_model_backwards}, + {"SGB Revision:", cycle_sgb_revision, current_sgb_revision_string, cycle_sgb_revision_backwards}, + {"Rewind Length:", cycle_rewind, current_rewind_string, cycle_rewind_backwards}, + {"Back", return_to_root_menu}, + {NULL,} +}; + +static void enter_emulation_menu(unsigned index) +{ + current_menu = emulation_menu; + current_selection = 0; + scroll = 0; +} + +const char *current_scaling_mode(unsigned index) +{ + return (const char *[]){"Fill Entire Window", "Retain Aspect Ratio", "Retain Integer Factor"} + [configuration.scaling_mode]; +} + +const char *current_default_scale(unsigned index) +{ + return (const char *[]){"1x", "2x", "3x", "4x", "5x", "6x", "7x", "8x"} + [configuration.default_scale - 1]; +} + +const char *current_color_correction_mode(unsigned index) +{ + return (const char *[]){"Disabled", "Correct Color Curves", "Emulate Hardware", "Preserve Brightness", "Reduce Contrast"} + [configuration.color_correction_mode]; +} + +const char *current_palette(unsigned index) +{ + return (const char *[]){"Greyscale", "Lime (Game Boy)", "Olive (Pocket)", "Teal (Light)"} + [configuration.dmg_palette]; +} + +const char *current_border_mode(unsigned index) +{ + return (const char *[]){"SGB Only", "Never", "Always"} + [configuration.border_mode]; +} + +void cycle_scaling(unsigned index) +{ + configuration.scaling_mode++; + if (configuration.scaling_mode == GB_SDL_SCALING_MAX) { + configuration.scaling_mode = 0; + } + update_viewport(); + render_texture(NULL, NULL); +} + +void cycle_scaling_backwards(unsigned index) +{ + if (configuration.scaling_mode == 0) { + configuration.scaling_mode = GB_SDL_SCALING_MAX - 1; + } + else { + configuration.scaling_mode--; + } + update_viewport(); + render_texture(NULL, NULL); +} + +void cycle_default_scale(unsigned index) +{ + if (configuration.default_scale == GB_SDL_DEFAULT_SCALE_MAX) { + configuration.default_scale = 1; + } + else { + configuration.default_scale++; + } + + rescale_window(); + update_viewport(); +} + +void cycle_default_scale_backwards(unsigned index) +{ + if (configuration.default_scale == 1) { + configuration.default_scale = GB_SDL_DEFAULT_SCALE_MAX; + } + else { + configuration.default_scale--; + } + + rescale_window(); + update_viewport(); +} + +static void cycle_color_correction(unsigned index) +{ + if (configuration.color_correction_mode == GB_COLOR_CORRECTION_REDUCE_CONTRAST) { + configuration.color_correction_mode = GB_COLOR_CORRECTION_DISABLED; + } + else { + configuration.color_correction_mode++; + } +} + +static void cycle_color_correction_backwards(unsigned index) +{ + if (configuration.color_correction_mode == GB_COLOR_CORRECTION_DISABLED) { + configuration.color_correction_mode = GB_COLOR_CORRECTION_REDUCE_CONTRAST; + } + else { + configuration.color_correction_mode--; + } +} + +static void cycle_palette(unsigned index) +{ + if (configuration.dmg_palette == 3) { + configuration.dmg_palette = 0; + } + else { + configuration.dmg_palette++; + } +} + +static void cycle_palette_backwards(unsigned index) +{ + if (configuration.dmg_palette == 0) { + configuration.dmg_palette = 3; + } + else { + configuration.dmg_palette--; + } +} + +static void cycle_border_mode(unsigned index) +{ + if (configuration.border_mode == GB_BORDER_ALWAYS) { + configuration.border_mode = GB_BORDER_SGB; + } + else { + configuration.border_mode++; + } +} + +static void cycle_border_mode_backwards(unsigned index) +{ + if (configuration.border_mode == GB_BORDER_SGB) { + configuration.border_mode = GB_BORDER_ALWAYS; + } + else { + configuration.border_mode--; + } +} + +struct shader_name { + const char *file_name; + const char *display_name; +} shaders[] = +{ + {"NearestNeighbor", "Nearest Neighbor"}, + {"Bilinear", "Bilinear"}, + {"SmoothBilinear", "Smooth Bilinear"}, + {"MonoLCD", "Monochrome LCD"}, + {"LCD", "LCD Display"}, + {"CRT", "CRT Display"}, + {"Scale2x", "Scale2x"}, + {"Scale4x", "Scale4x"}, + {"AAScale2x", "Anti-aliased Scale2x"}, + {"AAScale4x", "Anti-aliased Scale4x"}, + {"HQ2x", "HQ2x"}, + {"OmniScale", "OmniScale"}, + {"OmniScaleLegacy", "OmniScale Legacy"}, + {"AAOmniScaleLegacy", "AA OmniScale Legacy"}, +}; + +static void cycle_filter(unsigned index) +{ + unsigned i = 0; + for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) { + if (strcmp(shaders[i].file_name, configuration.filter) == 0) { + break; + } + } + + + i += 1; + if (i >= sizeof(shaders) / sizeof(shaders[0])) { + i -= sizeof(shaders) / sizeof(shaders[0]); + } + + strcpy(configuration.filter, shaders[i].file_name); + free_shader(&shader); + if (!init_shader_with_name(&shader, configuration.filter)) { + init_shader_with_name(&shader, "NearestNeighbor"); + } +} + +static void cycle_filter_backwards(unsigned index) +{ + unsigned i = 0; + for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) { + if (strcmp(shaders[i].file_name, configuration.filter) == 0) { + break; + } + } + + i -= 1; + if (i >= sizeof(shaders) / sizeof(shaders[0])) { + i = sizeof(shaders) / sizeof(shaders[0]) - 1; + } + + strcpy(configuration.filter, shaders[i].file_name); + free_shader(&shader); + if (!init_shader_with_name(&shader, configuration.filter)) { + init_shader_with_name(&shader, "NearestNeighbor"); + } + +} +const char *current_filter_name(unsigned index) +{ + unsigned i = 0; + for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) { + if (strcmp(shaders[i].file_name, configuration.filter) == 0) { + break; + } + } + + if (i == sizeof(shaders) / sizeof(shaders[0])) { + i = 0; + } + + return shaders[i].display_name; +} + +static void cycle_blending_mode(unsigned index) +{ + if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_ACCURATE) { + configuration.blending_mode = GB_FRAME_BLENDING_MODE_DISABLED; + } + else { + configuration.blending_mode++; + } +} + +static void cycle_blending_mode_backwards(unsigned index) +{ + if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_DISABLED) { + configuration.blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE; + } + else { + configuration.blending_mode--; + } +} + +const char *blending_mode_string(unsigned index) +{ + return (const char *[]){"Disabled", "Simple", "Accurate"} + [configuration.blending_mode]; +} + +static const struct menu_item graphics_menu[] = { + {"Scaling Mode:", cycle_scaling, current_scaling_mode, cycle_scaling_backwards}, + {"Default Window Scale:", cycle_default_scale, current_default_scale, cycle_default_scale_backwards}, + {"Scaling Filter:", cycle_filter, current_filter_name, cycle_filter_backwards}, + {"Color Correction:", cycle_color_correction, current_color_correction_mode, cycle_color_correction_backwards}, + {"Frame Blending:", cycle_blending_mode, blending_mode_string, cycle_blending_mode_backwards}, + {"Mono Palette:", cycle_palette, current_palette, cycle_palette_backwards}, + {"Display Border:", cycle_border_mode, current_border_mode, cycle_border_mode_backwards}, + {"Back", return_to_root_menu}, + {NULL,} +}; + +static void enter_graphics_menu(unsigned index) +{ + current_menu = graphics_menu; + current_selection = 0; + scroll = 0; +} + +const char *highpass_filter_string(unsigned index) +{ + return (const char *[]){"None (Keep DC Offset)", "Accurate", "Preserve Waveform"} + [configuration.highpass_mode]; +} + +void cycle_highpass_filter(unsigned index) +{ + configuration.highpass_mode++; + if (configuration.highpass_mode == GB_HIGHPASS_MAX) { + configuration.highpass_mode = 0; + } +} + +void cycle_highpass_filter_backwards(unsigned index) +{ + if (configuration.highpass_mode == 0) { + configuration.highpass_mode = GB_HIGHPASS_MAX - 1; + } + else { + configuration.highpass_mode--; + } +} + +const char *volume_string(unsigned index) +{ + static char ret[5]; + sprintf(ret, "%d%%", configuration.volume); + return ret; +} + +void increase_volume(unsigned index) +{ + configuration.volume += 5; + if (configuration.volume > 100) { + configuration.volume = 100; + } +} + +void decrease_volume(unsigned index) +{ + configuration.volume -= 5; + if (configuration.volume > 100) { + configuration.volume = 0; + } +} + +static const struct menu_item audio_menu[] = { + {"Highpass Filter:", cycle_highpass_filter, highpass_filter_string, cycle_highpass_filter_backwards}, + {"Volume:", increase_volume, volume_string, decrease_volume}, + {"Back", return_to_root_menu}, + {NULL,} +}; + +static void enter_audio_menu(unsigned index) +{ + current_menu = audio_menu; + current_selection = 0; + scroll = 0; +} + +static void modify_key(unsigned index) +{ + gui_state = WAITING_FOR_KEY; +} + +static const char *key_name(unsigned index); + +static const struct menu_item controls_menu[] = { + {"Right:", modify_key, key_name,}, + {"Left:", modify_key, key_name,}, + {"Up:", modify_key, key_name,}, + {"Down:", modify_key, key_name,}, + {"A:", modify_key, key_name,}, + {"B:", modify_key, key_name,}, + {"Select:", modify_key, key_name,}, + {"Start:", modify_key, key_name,}, + {"Turbo:", modify_key, key_name,}, + {"Rewind:", modify_key, key_name,}, + {"Slow-Motion:", modify_key, key_name,}, + {"Back", return_to_root_menu}, + {NULL,} +}; + +static const char *key_name(unsigned index) +{ + if (index >= 8) { + if (index == 8) { + return SDL_GetScancodeName(configuration.keys[8]); + } + return SDL_GetScancodeName(configuration.keys_2[index - 9]); + } + return SDL_GetScancodeName(configuration.keys[index]); +} + +static void enter_controls_menu(unsigned index) +{ + current_menu = controls_menu; + current_selection = 0; + scroll = 0; +} + +static unsigned joypad_index = 0; +static SDL_Joystick *joystick = NULL; +static SDL_GameController *controller = NULL; +SDL_Haptic *haptic = NULL; + +const char *current_joypad_name(unsigned index) +{ + static char name[23] = {0,}; + const char *orig_name = joystick? SDL_JoystickName(joystick) : NULL; + if (!orig_name) return "Not Found"; + unsigned i = 0; + + // SDL returns a name with repeated and trailing spaces + while (*orig_name && i < sizeof(name) - 2) { + if (orig_name[0] != ' ' || orig_name[1] != ' ') { + name[i++] = *orig_name; + } + orig_name++; + } + if (i && name[i - 1] == ' ') { + i--; + } + name[i] = 0; + + return name; +} + +static void cycle_joypads(unsigned index) +{ + joypad_index++; + if (joypad_index >= SDL_NumJoysticks()) { + joypad_index = 0; + } + + if (haptic) { + SDL_HapticClose(haptic); + haptic = NULL; + } + + if (controller) { + SDL_GameControllerClose(controller); + controller = NULL; + } + else if (joystick) { + SDL_JoystickClose(joystick); + joystick = NULL; + } + if ((controller = SDL_GameControllerOpen(joypad_index))) { + joystick = SDL_GameControllerGetJoystick(controller); + } + else { + joystick = SDL_JoystickOpen(joypad_index); + } + if (joystick) { + haptic = SDL_HapticOpenFromJoystick(joystick); + }} + +static void cycle_joypads_backwards(unsigned index) +{ + joypad_index--; + if (joypad_index >= SDL_NumJoysticks()) { + joypad_index = SDL_NumJoysticks() - 1; + } + + if (haptic) { + SDL_HapticClose(haptic); + haptic = NULL; + } + + if (controller) { + SDL_GameControllerClose(controller); + controller = NULL; + } + else if (joystick) { + SDL_JoystickClose(joystick); + joystick = NULL; + } + if ((controller = SDL_GameControllerOpen(joypad_index))) { + joystick = SDL_GameControllerGetJoystick(controller); + } + else { + joystick = SDL_JoystickOpen(joypad_index); + } + if (joystick) { + haptic = SDL_HapticOpenFromJoystick(joystick); + }} + +static void detect_joypad_layout(unsigned index) +{ + gui_state = WAITING_FOR_JBUTTON; + joypad_configuration_progress = 0; + joypad_axis_temp = -1; +} + +static void cycle_rumble_mode(unsigned index) +{ + if (configuration.rumble_mode == GB_RUMBLE_ALL_GAMES) { + configuration.rumble_mode = GB_RUMBLE_DISABLED; + } + else { + configuration.rumble_mode++; + } +} + +static void cycle_rumble_mode_backwards(unsigned index) +{ + if (configuration.rumble_mode == GB_RUMBLE_DISABLED) { + configuration.rumble_mode = GB_RUMBLE_ALL_GAMES; + } + else { + configuration.rumble_mode--; + } +} + +const char *current_rumble_mode(unsigned index) +{ + return (const char *[]){"Disabled", "Rumble Game Paks Only", "All Games"} + [configuration.rumble_mode]; +} + +static const struct menu_item joypad_menu[] = { + {"Joypad:", cycle_joypads, current_joypad_name, cycle_joypads_backwards}, + {"Configure layout", detect_joypad_layout}, + {"Rumble Mode:", cycle_rumble_mode, current_rumble_mode, cycle_rumble_mode_backwards}, + {"Back", return_to_root_menu}, + {NULL,} +}; + +static void enter_joypad_menu(unsigned index) +{ + current_menu = joypad_menu; + current_selection = 0; + scroll = 0; +} + +joypad_button_t get_joypad_button(uint8_t physical_button) +{ + for (unsigned i = 0; i < JOYPAD_BUTTONS_MAX; i++) { + if (configuration.joypad_configuration[i] == physical_button) { + return i; + } + } + return JOYPAD_BUTTONS_MAX; +} + +joypad_axis_t get_joypad_axis(uint8_t physical_axis) +{ + for (unsigned i = 0; i < JOYPAD_AXISES_MAX; i++) { + if (configuration.joypad_axises[i] == physical_axis) { + return i; + } + } + return JOYPAD_AXISES_MAX; +} + + +void connect_joypad(void) +{ + if (joystick && !SDL_NumJoysticks()) { + if (controller) { + SDL_GameControllerClose(controller); + controller = NULL; + joystick = NULL; + } + else { + SDL_JoystickClose(joystick); + joystick = NULL; + } + } + else if (!joystick && SDL_NumJoysticks()) { + if ((controller = SDL_GameControllerOpen(0))) { + joystick = SDL_GameControllerGetJoystick(controller); + } + else { + joystick = SDL_JoystickOpen(0); + } + } + if (joystick) { + haptic = SDL_HapticOpenFromJoystick(joystick); + } +} + +void run_gui(bool is_running) +{ + SDL_ShowCursor(SDL_ENABLE); + connect_joypad(); + + /* Draw the background screen */ + static SDL_Surface *converted_background = NULL; + if (!converted_background) { + SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp")); + SDL_SetPaletteColors(background->format->palette, gui_palette, 0, 4); + converted_background = SDL_ConvertSurface(background, pixel_format, 0); + SDL_LockSurface(converted_background); + SDL_FreeSurface(background); + + for (unsigned i = 4; i--; ) { + gui_palette_native[i] = SDL_MapRGB(pixel_format, gui_palette[i].r, gui_palette[i].g, gui_palette[i].b); + } + } + + unsigned width = GB_get_screen_width(&gb); + unsigned height = GB_get_screen_height(&gb); + unsigned x_offset = (width - 160) / 2; + unsigned y_offset = (height - 144) / 2; + uint32_t pixels[width * height]; + + if (width != 160 || height != 144) { + for (unsigned i = 0; i < width * height; i++) { + pixels[i] = gui_palette_native[0]; + } + } + + SDL_Event event = {0,}; + gui_state = is_running? SHOWING_MENU : SHOWING_DROP_MESSAGE; + bool should_render = true; + current_menu = root_menu = is_running? paused_menu : nonpaused_menu; + current_selection = 0; + scroll = 0; + do { + /* Convert Joypad and mouse events (We only generate down events) */ + if (gui_state != WAITING_FOR_KEY && gui_state != WAITING_FOR_JBUTTON) { + switch (event.type) { + case SDL_WINDOWEVENT: + should_render = true; + break; + case SDL_MOUSEBUTTONDOWN: + if (gui_state == SHOWING_HELP) { + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + } + else if (gui_state == SHOWING_DROP_MESSAGE) { + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; + } + else if (gui_state == SHOWING_MENU) { + signed x = (event.button.x - rect.x / factor) * width / (rect.w / factor) - x_offset; + signed y = (event.button.y - rect.y / factor) * height / (rect.h / factor) - y_offset; + + if (strcmp("CRT", configuration.filter) == 0) { + y = y * 8 / 7; + y -= 144 / 16; + } + y += scroll; + + if (x < 0 || x >= 160 || y < 24) { + continue; + } + + unsigned item_y = 24; + unsigned index = 0; + for (const struct menu_item *item = current_menu; item->string; item++, index++) { + if (!item->backwards_handler) { + if (y >= item_y && y < item_y + 12) { + break; + } + item_y += 12; + } + else { + if (y >= item_y && y < item_y + 24) { + break; + } + item_y += 24; + } + } + + if (!current_menu[index].string) continue; + + current_selection = index; + event.type = SDL_KEYDOWN; + if (current_menu[index].backwards_handler) { + event.key.keysym.scancode = x < 80? SDL_SCANCODE_LEFT : SDL_SCANCODE_RIGHT; + } + else { + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + } + + } + break; + case SDL_JOYBUTTONDOWN: + event.type = SDL_KEYDOWN; + joypad_button_t button = get_joypad_button(event.jbutton.button); + if (button == JOYPAD_BUTTON_A) { + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + } + else if (button == JOYPAD_BUTTON_MENU) { + event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; + } + else if (button == JOYPAD_BUTTON_UP) event.key.keysym.scancode = SDL_SCANCODE_UP; + else if (button == JOYPAD_BUTTON_DOWN) event.key.keysym.scancode = SDL_SCANCODE_DOWN; + else if (button == JOYPAD_BUTTON_LEFT) event.key.keysym.scancode = SDL_SCANCODE_LEFT; + else if (button == JOYPAD_BUTTON_RIGHT) event.key.keysym.scancode = SDL_SCANCODE_RIGHT; + break; + + case SDL_JOYHATMOTION: { + uint8_t value = event.jhat.value; + if (value != 0) { + uint32_t scancode = + value == SDL_HAT_UP ? SDL_SCANCODE_UP + : value == SDL_HAT_DOWN ? SDL_SCANCODE_DOWN + : value == SDL_HAT_LEFT ? SDL_SCANCODE_LEFT + : value == SDL_HAT_RIGHT ? SDL_SCANCODE_RIGHT + : 0; + + if (scancode != 0) { + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = scancode; + } + } + break; + } + + case SDL_JOYAXISMOTION: { + static bool axis_active[2] = {false, false}; + joypad_axis_t axis = get_joypad_axis(event.jaxis.axis); + if (axis == JOYPAD_AXISES_X) { + if (!axis_active[0] && event.jaxis.value > JOYSTICK_HIGH) { + axis_active[0] = true; + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_RIGHT; + } + else if (!axis_active[0] && event.jaxis.value < -JOYSTICK_HIGH) { + axis_active[0] = true; + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_LEFT; + + } + else if (axis_active[0] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) { + axis_active[0] = false; + } + } + else if (axis == JOYPAD_AXISES_Y) { + if (!axis_active[1] && event.jaxis.value > JOYSTICK_HIGH) { + axis_active[1] = true; + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_DOWN; + } + else if (!axis_active[1] && event.jaxis.value < -JOYSTICK_HIGH) { + axis_active[1] = true; + event.type = SDL_KEYDOWN; + event.key.keysym.scancode = SDL_SCANCODE_UP; + } + else if (axis_active[1] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) { + axis_active[1] = false; + } + } + } + } + } + switch (event.type) { + case SDL_QUIT: { + if (!is_running) { + exit(0); + } + else { + pending_command = GB_SDL_QUIT_COMMAND; + return; + } + + } + case SDL_WINDOWEVENT: { + if (event.window.event == SDL_WINDOWEVENT_RESIZED) { + update_viewport(); + render_texture(NULL, NULL); + } + break; + } + case SDL_DROPFILE: { + set_filename(event.drop.file, SDL_free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + return; + } + case SDL_JOYBUTTONDOWN: + { + if (gui_state == WAITING_FOR_JBUTTON && joypad_configuration_progress != JOYPAD_BUTTONS_MAX) { + should_render = true; + configuration.joypad_configuration[joypad_configuration_progress++] = event.jbutton.button; + } + break; + } + + case SDL_JOYAXISMOTION: { + if (gui_state == WAITING_FOR_JBUTTON && + joypad_configuration_progress == JOYPAD_BUTTONS_MAX && + abs(event.jaxis.value) >= 0x4000) { + if (joypad_axis_temp == (uint8_t)-1) { + joypad_axis_temp = event.jaxis.axis; + } + else if (joypad_axis_temp != event.jaxis.axis) { + if (joypad_axis_temp < event.jaxis.axis) { + configuration.joypad_axises[JOYPAD_AXISES_X] = joypad_axis_temp; + configuration.joypad_axises[JOYPAD_AXISES_Y] = event.jaxis.axis; + } + else { + configuration.joypad_axises[JOYPAD_AXISES_Y] = joypad_axis_temp; + configuration.joypad_axises[JOYPAD_AXISES_X] = event.jaxis.axis; + } + + gui_state = SHOWING_MENU; + should_render = true; + } + } + break; + } + + case SDL_KEYDOWN: + if (event.key.keysym.scancode == SDL_SCANCODE_F && event.key.keysym.mod & MODIFIER) { + if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) { + SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); + } + else { + SDL_SetWindowFullscreen(window, 0); + } + update_viewport(); + } + if (event.key.keysym.scancode == SDL_SCANCODE_O) { + if (event.key.keysym.mod & MODIFIER) { + char *filename = do_open_rom_dialog(); + if (filename) { + set_filename(filename, free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + return; + } + } + } + else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && gui_state == WAITING_FOR_JBUTTON) { + should_render = true; + if (joypad_configuration_progress != JOYPAD_BUTTONS_MAX) { + configuration.joypad_configuration[joypad_configuration_progress] = -1; + } + else { + configuration.joypad_axises[0] = -1; + configuration.joypad_axises[1] = -1; + } + joypad_configuration_progress++; + + if (joypad_configuration_progress > JOYPAD_BUTTONS_MAX) { + gui_state = SHOWING_MENU; + } + } + else if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { + if (is_running) { + return; + } + else { + if (gui_state == SHOWING_DROP_MESSAGE) { + gui_state = SHOWING_MENU; + } + else if (gui_state == SHOWING_MENU) { + gui_state = SHOWING_DROP_MESSAGE; + } + current_selection = 0; + scroll = 0; + current_menu = root_menu; + should_render = true; + } + } + else if (gui_state == SHOWING_MENU) { + if (event.key.keysym.scancode == SDL_SCANCODE_DOWN && current_menu[current_selection + 1].string) { + current_selection++; + should_render = true; + } + else if (event.key.keysym.scancode == SDL_SCANCODE_UP && current_selection) { + current_selection--; + should_render = true; + } + else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && !current_menu[current_selection].backwards_handler) { + if (current_menu[current_selection].handler) { + current_menu[current_selection].handler(current_selection); + if (pending_command == GB_SDL_RESET_COMMAND && !is_running) { + pending_command = GB_SDL_NO_COMMAND; + } + if (pending_command) { + if (!is_running && pending_command == GB_SDL_QUIT_COMMAND) { + exit(0); + } + return; + } + should_render = true; + } + else { + return; + } + } + else if (event.key.keysym.scancode == SDL_SCANCODE_RIGHT && current_menu[current_selection].backwards_handler) { + current_menu[current_selection].handler(current_selection); + should_render = true; + } + else if (event.key.keysym.scancode == SDL_SCANCODE_LEFT && current_menu[current_selection].backwards_handler) { + current_menu[current_selection].backwards_handler(current_selection); + should_render = true; + } + } + else if (gui_state == SHOWING_HELP) { + current_help_page++; + if (current_help_page == sizeof(help) / sizeof(help[0])) { + gui_state = SHOWING_MENU; + } + should_render = true; + } + else if (gui_state == WAITING_FOR_KEY) { + if (current_selection >= 8) { + if (current_selection == 8) { + configuration.keys[8] = event.key.keysym.scancode; + } + else { + configuration.keys_2[current_selection - 9] = event.key.keysym.scancode; + } + } + else { + configuration.keys[current_selection] = event.key.keysym.scancode; + } + gui_state = SHOWING_MENU; + should_render = true; + } + break; + } + + if (should_render) { + should_render = false; + rerender: + if (width == 160 && height == 144) { + memcpy(pixels, converted_background->pixels, sizeof(pixels)); + } + else { + for (unsigned y = 0; y < 144; y++) { + memcpy(pixels + x_offset + width * (y + y_offset), ((uint32_t *)converted_background->pixels) + 160 * y, 160 * 4); + } + } + + switch (gui_state) { + case SHOWING_DROP_MESSAGE: + draw_text_centered(pixels, width, height, 8 + y_offset, "Press ESC for menu", gui_palette_native[3], gui_palette_native[0], false); + draw_text_centered(pixels, width, height, 116 + y_offset, "Drop a GB or GBC", gui_palette_native[3], gui_palette_native[0], false); + draw_text_centered(pixels, width, height, 128 + y_offset, "file to play", gui_palette_native[3], gui_palette_native[0], false); + break; + case SHOWING_MENU: + draw_text_centered(pixels, width, height, 8 + y_offset, "SameBoy", gui_palette_native[3], gui_palette_native[0], false); + unsigned i = 0, y = 24; + for (const struct menu_item *item = current_menu; item->string; item++, i++) { + if (i == current_selection) { + if (y < scroll) { + scroll = y - 4; + goto rerender; + } + } + if (i == current_selection && i == 0 && scroll != 0) { + scroll = 0; + goto rerender; + } + if (item->value_getter && !item->backwards_handler) { + char line[25]; + snprintf(line, sizeof(line), "%s%*s", item->string, 24 - (unsigned)strlen(item->string), item->value_getter(i)); + draw_text_centered(pixels, width, height, y + y_offset, line, gui_palette_native[3], gui_palette_native[0], + i == current_selection ? DECORATION_SELECTION : DECORATION_NONE); + y += 12; + + } + else { + draw_text_centered(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0], + i == current_selection && !item->value_getter ? DECORATION_SELECTION : DECORATION_NONE); + y += 12; + if (item->value_getter) { + draw_text_centered(pixels, width, height, y + y_offset, item->value_getter(i), gui_palette_native[3], gui_palette_native[0], + i == current_selection ? DECORATION_ARROWS : DECORATION_NONE); + y += 12; + } + } + if (i == current_selection) { + if (y > scroll + 144) { + scroll = y - 144; + goto rerender; + } + } + + } + break; + case SHOWING_HELP: + draw_text(pixels, width, height, 2 + x_offset, 2 + y_offset, help[current_help_page], gui_palette_native[3], gui_palette_native[0]); + break; + case WAITING_FOR_KEY: + draw_text_centered(pixels, width, height, 68 + y_offset, "Press a Key", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + break; + case WAITING_FOR_JBUTTON: + draw_text_centered(pixels, width, height, 68 + y_offset, + joypad_configuration_progress != JOYPAD_BUTTONS_MAX ? "Press button for" : "Move the Analog Stick", + gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + draw_text_centered(pixels, width, height, 80 + y_offset, + (const char *[]) + { + "Right", + "Left", + "Up", + "Down", + "A", + "B", + "Select", + "Start", + "Open Menu", + "Turbo", + "Rewind", + "Slow-Motion", + "", + } [joypad_configuration_progress], + gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + draw_text_centered(pixels, width, height, 104 + y_offset, "Press Enter to skip", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + break; + } + + render_texture(pixels, NULL); +#ifdef _WIN32 + /* Required for some Windows 10 machines, god knows why */ + render_texture(pixels, NULL); +#endif + } + } while (SDL_WaitEvent(&event)); +} diff --git a/bsnes/gb/SDL/gui.h b/bsnes/gb/SDL/gui.h new file mode 100644 index 00000000..c9509076 --- /dev/null +++ b/bsnes/gb/SDL/gui.h @@ -0,0 +1,125 @@ +#ifndef gui_h +#define gui_h + +#include +#include +#include +#include "shader.h" + +#define JOYSTICK_HIGH 0x4000 +#define JOYSTICK_LOW 0x3800 + +#ifdef __APPLE__ +#define MODIFIER KMOD_GUI +#else +#define MODIFIER KMOD_CTRL +#endif + +extern GB_gameboy_t gb; + +extern SDL_Window *window; +extern SDL_Renderer *renderer; +extern SDL_Texture *texture; +extern SDL_PixelFormat *pixel_format; +extern SDL_Haptic *haptic; +extern shader_t shader; + +enum scaling_mode { + GB_SDL_SCALING_ENTIRE_WINDOW, + GB_SDL_SCALING_KEEP_RATIO, + GB_SDL_SCALING_INTEGER_FACTOR, + GB_SDL_SCALING_MAX, +}; + + +enum pending_command { + GB_SDL_NO_COMMAND, + GB_SDL_SAVE_STATE_COMMAND, + GB_SDL_LOAD_STATE_COMMAND, + GB_SDL_RESET_COMMAND, + GB_SDL_NEW_FILE_COMMAND, + GB_SDL_QUIT_COMMAND, +}; + +#define GB_SDL_DEFAULT_SCALE_MAX 8 + +extern enum pending_command pending_command; +extern unsigned command_parameter; + +typedef enum { + JOYPAD_BUTTON_LEFT, + JOYPAD_BUTTON_RIGHT, + JOYPAD_BUTTON_UP, + JOYPAD_BUTTON_DOWN, + JOYPAD_BUTTON_A, + JOYPAD_BUTTON_B, + JOYPAD_BUTTON_SELECT, + JOYPAD_BUTTON_START, + JOYPAD_BUTTON_MENU, + JOYPAD_BUTTON_TURBO, + JOYPAD_BUTTON_REWIND, + JOYPAD_BUTTON_SLOW_MOTION, + JOYPAD_BUTTONS_MAX +} joypad_button_t; + +typedef enum { + JOYPAD_AXISES_X, + JOYPAD_AXISES_Y, + JOYPAD_AXISES_MAX +} joypad_axis_t; + +typedef struct { + SDL_Scancode keys[9]; + GB_color_correction_mode_t color_correction_mode; + enum scaling_mode scaling_mode; + uint8_t blending_mode; + + GB_highpass_mode_t highpass_mode; + + bool _deprecated_div_joystick; + bool _deprecated_flip_joystick_bit_1; + bool _deprecated_swap_joysticks_bits_1_and_2; + + char filter[32]; + enum { + MODEL_DMG, + MODEL_CGB, + MODEL_AGB, + MODEL_SGB, + MODEL_MAX, + } model; + + /* v0.11 */ + uint32_t rewind_length; + SDL_Scancode keys_2[32]; /* Rewind and underclock, + padding for the future */ + uint8_t joypad_configuration[32]; /* 12 Keys + padding for the future*/; + uint8_t joypad_axises[JOYPAD_AXISES_MAX]; + + /* v0.12 */ + enum { + SGB_NTSC, + SGB_PAL, + SGB_2, + SGB_MAX + } sgb_revision; + + /* v0.13 */ + uint8_t dmg_palette; + GB_border_mode_t border_mode; + uint8_t volume; + GB_rumble_mode_t rumble_mode; + + uint8_t default_scale; +} configuration_t; + +extern configuration_t configuration; + +void update_viewport(void); +void run_gui(bool is_running); +void render_texture(void *pixels, void *previous); +void connect_joypad(void); + +joypad_button_t get_joypad_button(uint8_t physical_button); +joypad_axis_t get_joypad_axis(uint8_t physical_axis); + +#endif diff --git a/bsnes/gb/SDL/main.c b/bsnes/gb/SDL/main.c new file mode 100644 index 00000000..e79d0b33 --- /dev/null +++ b/bsnes/gb/SDL/main.c @@ -0,0 +1,712 @@ +#include +#include +#include +#include +#include +#include +#include +#include "utils.h" +#include "gui.h" +#include "shader.h" +#include "audio/audio.h" + +#ifndef _WIN32 +#include +#else +#include +#endif + +GB_gameboy_t gb; +static bool paused = false; +static uint32_t pixel_buffer_1[256 * 224], pixel_buffer_2[256 * 224]; +static uint32_t *active_pixel_buffer = pixel_buffer_1, *previous_pixel_buffer = pixel_buffer_2; +static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false; +static double clock_mutliplier = 1.0; + +static char *filename = NULL; +static typeof(free) *free_function = NULL; +static char *battery_save_path_ptr; + + +void set_filename(const char *new_filename, typeof(free) *new_free_function) +{ + if (filename && free_function) { + free_function(filename); + } + filename = (char *) new_filename; + free_function = new_free_function; +} + +static char *captured_log = NULL; + +static void log_capture_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + size_t current_len = strlen(captured_log); + size_t len_to_add = strlen(string); + captured_log = realloc(captured_log, current_len + len_to_add + 1); + memcpy(captured_log + current_len, string, len_to_add); + captured_log[current_len + len_to_add] = 0; +} + +static void start_capturing_logs(void) +{ + if (captured_log != NULL) { + free(captured_log); + } + captured_log = malloc(1); + captured_log[0] = 0; + GB_set_log_callback(&gb, log_capture_callback); +} + +static const char *end_capturing_logs(bool show_popup, bool should_exit) +{ + GB_set_log_callback(&gb, NULL); + if (captured_log[0] == 0) { + free(captured_log); + captured_log = NULL; + } + else { + if (show_popup) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", captured_log, window); + } + if (should_exit) { + exit(1); + } + } + return captured_log; +} + +static void update_palette(void) +{ + switch (configuration.dmg_palette) { + case 1: + GB_set_palette(&gb, &GB_PALETTE_DMG); + break; + + case 2: + GB_set_palette(&gb, &GB_PALETTE_MGB); + break; + + case 3: + GB_set_palette(&gb, &GB_PALETTE_GBL); + break; + + default: + GB_set_palette(&gb, &GB_PALETTE_GREY); + } +} + +static void screen_size_changed(void) +{ + SDL_DestroyTexture(texture); + texture = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_STREAMING, + GB_get_screen_width(&gb), GB_get_screen_height(&gb)); + + SDL_SetWindowMinimumSize(window, GB_get_screen_width(&gb), GB_get_screen_height(&gb)); + + update_viewport(); +} + +static void open_menu(void) +{ + bool audio_playing = GB_audio_is_playing(); + if (audio_playing) { + GB_audio_set_paused(true); + } + size_t previous_width = GB_get_screen_width(&gb); + run_gui(true); + SDL_ShowCursor(SDL_DISABLE); + if (audio_playing) { + GB_audio_set_paused(false); + } + GB_set_color_correction_mode(&gb, configuration.color_correction_mode); + GB_set_border_mode(&gb, configuration.border_mode); + update_palette(); + GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); + if (previous_width != GB_get_screen_width(&gb)) { + screen_size_changed(); + } +} + +static void handle_events(GB_gameboy_t *gb) +{ + SDL_Event event; + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + pending_command = GB_SDL_QUIT_COMMAND; + break; + + case SDL_DROPFILE: { + set_filename(event.drop.file, SDL_free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + break; + } + + case SDL_WINDOWEVENT: { + if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + update_viewport(); + } + break; + } + + case SDL_JOYBUTTONUP: + case SDL_JOYBUTTONDOWN: { + joypad_button_t button = get_joypad_button(event.jbutton.button); + if ((GB_key_t) button < GB_KEY_MAX) { + GB_set_key_state(gb, (GB_key_t) button, event.type == SDL_JOYBUTTONDOWN); + } + else if (button == JOYPAD_BUTTON_TURBO) { + GB_audio_clear_queue(); + turbo_down = event.type == SDL_JOYBUTTONDOWN; + GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + } + else if (button == JOYPAD_BUTTON_SLOW_MOTION) { + underclock_down = event.type == SDL_JOYBUTTONDOWN; + } + else if (button == JOYPAD_BUTTON_REWIND) { + rewind_down = event.type == SDL_JOYBUTTONDOWN; + if (event.type == SDL_JOYBUTTONUP) { + rewind_paused = false; + } + GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + } + else if (button == JOYPAD_BUTTON_MENU && event.type == SDL_JOYBUTTONDOWN) { + open_menu(); + } + } + break; + + case SDL_JOYAXISMOTION: { + static bool axis_active[2] = {false, false}; + joypad_axis_t axis = get_joypad_axis(event.jaxis.axis); + if (axis == JOYPAD_AXISES_X) { + if (event.jaxis.value > JOYSTICK_HIGH) { + axis_active[0] = true; + GB_set_key_state(gb, GB_KEY_RIGHT, true); + GB_set_key_state(gb, GB_KEY_LEFT, false); + } + else if (event.jaxis.value < -JOYSTICK_HIGH) { + axis_active[0] = true; + GB_set_key_state(gb, GB_KEY_RIGHT, false); + GB_set_key_state(gb, GB_KEY_LEFT, true); + } + else if (axis_active[0] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) { + axis_active[0] = false; + GB_set_key_state(gb, GB_KEY_RIGHT, false); + GB_set_key_state(gb, GB_KEY_LEFT, false); + } + } + else if (axis == JOYPAD_AXISES_Y) { + if (event.jaxis.value > JOYSTICK_HIGH) { + axis_active[1] = true; + GB_set_key_state(gb, GB_KEY_DOWN, true); + GB_set_key_state(gb, GB_KEY_UP, false); + } + else if (event.jaxis.value < -JOYSTICK_HIGH) { + axis_active[1] = true; + GB_set_key_state(gb, GB_KEY_DOWN, false); + GB_set_key_state(gb, GB_KEY_UP, true); + } + else if (axis_active[1] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) { + axis_active[1] = false; + GB_set_key_state(gb, GB_KEY_DOWN, false); + GB_set_key_state(gb, GB_KEY_UP, false); + } + } + } + break; + + case SDL_JOYHATMOTION: + { + uint8_t value = event.jhat.value; + int8_t updown = + value == SDL_HAT_LEFTUP || value == SDL_HAT_UP || value == SDL_HAT_RIGHTUP ? -1 : (value == SDL_HAT_LEFTDOWN || value == SDL_HAT_DOWN || value == SDL_HAT_RIGHTDOWN ? 1 : 0); + int8_t leftright = + value == SDL_HAT_LEFTUP || value == SDL_HAT_LEFT || value == SDL_HAT_LEFTDOWN ? -1 : (value == SDL_HAT_RIGHTUP || value == SDL_HAT_RIGHT || value == SDL_HAT_RIGHTDOWN ? 1 : 0); + + GB_set_key_state(gb, GB_KEY_LEFT, leftright == -1); + GB_set_key_state(gb, GB_KEY_RIGHT, leftright == 1); + GB_set_key_state(gb, GB_KEY_UP, updown == -1); + GB_set_key_state(gb, GB_KEY_DOWN, updown == 1); + break; + }; + + case SDL_KEYDOWN: + switch (event.key.keysym.scancode) { + case SDL_SCANCODE_ESCAPE: { + open_menu(); + break; + } + case SDL_SCANCODE_C: + if (event.type == SDL_KEYDOWN && (event.key.keysym.mod & KMOD_CTRL)) { + GB_debugger_break(gb); + + } + break; + + case SDL_SCANCODE_R: + if (event.key.keysym.mod & MODIFIER) { + pending_command = GB_SDL_RESET_COMMAND; + } + break; + + case SDL_SCANCODE_O: { + if (event.key.keysym.mod & MODIFIER) { + char *filename = do_open_rom_dialog(); + if (filename) { + set_filename(filename, free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + } + } + break; + } + + case SDL_SCANCODE_P: + if (event.key.keysym.mod & MODIFIER) { + paused = !paused; + } + break; + case SDL_SCANCODE_M: + if (event.key.keysym.mod & MODIFIER) { +#ifdef __APPLE__ + // Can't override CMD+M (Minimize) in SDL + if (!(event.key.keysym.mod & KMOD_SHIFT)) { + break; + } +#endif + GB_audio_set_paused(GB_audio_is_playing()); + } + break; + + case SDL_SCANCODE_F: + if (event.key.keysym.mod & MODIFIER) { + if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) { + SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); + } + else { + SDL_SetWindowFullscreen(window, 0); + } + update_viewport(); + } + break; + + default: + /* Save states */ + if (event.key.keysym.scancode >= SDL_SCANCODE_1 && event.key.keysym.scancode <= SDL_SCANCODE_0) { + if (event.key.keysym.mod & MODIFIER) { + command_parameter = (event.key.keysym.scancode - SDL_SCANCODE_1 + 1) % 10; + + if (event.key.keysym.mod & KMOD_SHIFT) { + pending_command = GB_SDL_LOAD_STATE_COMMAND; + } + else { + pending_command = GB_SDL_SAVE_STATE_COMMAND; + } + } + } + break; + } + case SDL_KEYUP: // Fallthrough + if (event.key.keysym.scancode == configuration.keys[8]) { + turbo_down = event.type == SDL_KEYDOWN; + GB_audio_clear_queue(); + GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + } + else if (event.key.keysym.scancode == configuration.keys_2[0]) { + rewind_down = event.type == SDL_KEYDOWN; + if (event.type == SDL_KEYUP) { + rewind_paused = false; + } + GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + } + else if (event.key.keysym.scancode == configuration.keys_2[1]) { + underclock_down = event.type == SDL_KEYDOWN; + } + else { + for (unsigned i = 0; i < GB_KEY_MAX; i++) { + if (event.key.keysym.scancode == configuration.keys[i]) { + GB_set_key_state(gb, i, event.type == SDL_KEYDOWN); + } + } + } + break; + default: + break; + } + } + } + +static void vblank(GB_gameboy_t *gb) +{ + if (underclock_down && clock_mutliplier > 0.5) { + clock_mutliplier -= 1.0/16; + GB_set_clock_multiplier(gb, clock_mutliplier); + } + else if (!underclock_down && clock_mutliplier < 1.0) { + clock_mutliplier += 1.0/16; + GB_set_clock_multiplier(gb, clock_mutliplier); + } + if (configuration.blending_mode) { + render_texture(active_pixel_buffer, previous_pixel_buffer); + uint32_t *temp = active_pixel_buffer; + active_pixel_buffer = previous_pixel_buffer; + previous_pixel_buffer = temp; + GB_set_pixels_output(gb, active_pixel_buffer); + } + else { + render_texture(active_pixel_buffer, NULL); + } + do_rewind = rewind_down; + handle_events(gb); +} + + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return SDL_MapRGB(pixel_format, r, g, b); +} + +static void rumble(GB_gameboy_t *gb, double amp) +{ + SDL_HapticRumblePlay(haptic, amp, 250); +} + +static void debugger_interrupt(int ignore) +{ + if (!GB_is_inited(&gb)) return; + /* ^C twice to exit */ + if (GB_debugger_is_stopped(&gb)) { + GB_save_battery(&gb, battery_save_path_ptr); + exit(0); + } + GB_debugger_break(&gb); +} + +static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) +{ + if (turbo_down) { + static unsigned skip = 0; + skip++; + if (skip == GB_audio_get_frequency() / 8) { + skip = 0; + } + if (skip > GB_audio_get_frequency() / 16) { + return; + } + } + + if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_frequency() / 4) { + return; + } + + if (configuration.volume != 100) { + sample->left = sample->left * configuration.volume / 100; + sample->right = sample->right * configuration.volume / 100; + } + + GB_audio_queue_sample(sample); + +} + + +static bool handle_pending_command(void) +{ + switch (pending_command) { + case GB_SDL_LOAD_STATE_COMMAND: + case GB_SDL_SAVE_STATE_COMMAND: { + char save_path[strlen(filename) + 4]; + char save_extension[] = ".s0"; + save_extension[2] += command_parameter; + replace_extension(filename, strlen(filename), save_path, save_extension); + + start_capturing_logs(); + if (pending_command == GB_SDL_LOAD_STATE_COMMAND) { + GB_load_state(&gb, save_path); + } + else { + GB_save_state(&gb, save_path); + } + end_capturing_logs(true, false); + return false; + } + + case GB_SDL_NO_COMMAND: + return false; + + case GB_SDL_RESET_COMMAND: + case GB_SDL_NEW_FILE_COMMAND: + GB_save_battery(&gb, battery_save_path_ptr); + return true; + + case GB_SDL_QUIT_COMMAND: + GB_save_battery(&gb, battery_save_path_ptr); + exit(0); + } + return false; +} + +static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) +{ + bool error = false; + start_capturing_logs(); + static const char *const names[] = { + [GB_BOOT_ROM_DMG0] = "dmg0_boot.bin", + [GB_BOOT_ROM_DMG] = "dmg_boot.bin", + [GB_BOOT_ROM_MGB] = "mgb_boot.bin", + [GB_BOOT_ROM_SGB] = "sgb_boot.bin", + [GB_BOOT_ROM_SGB2] = "sgb2_boot.bin", + [GB_BOOT_ROM_CGB0] = "cgb0_boot.bin", + [GB_BOOT_ROM_CGB] = "cgb_boot.bin", + [GB_BOOT_ROM_AGB] = "agb_boot.bin", + }; + GB_load_boot_rom(gb, resource_path(names[type])); + end_capturing_logs(true, error); +} + +static void run(void) +{ + SDL_ShowCursor(SDL_DISABLE); + GB_model_t model; + pending_command = GB_SDL_NO_COMMAND; +restart: + model = (GB_model_t []) + { + [MODEL_DMG] = GB_MODEL_DMG_B, + [MODEL_CGB] = GB_MODEL_CGB_E, + [MODEL_AGB] = GB_MODEL_AGB, + [MODEL_SGB] = (GB_model_t []) + { + [SGB_NTSC] = GB_MODEL_SGB_NTSC, + [SGB_PAL] = GB_MODEL_SGB_PAL, + [SGB_2] = GB_MODEL_SGB2, + }[configuration.sgb_revision], + }[configuration.model]; + + if (GB_is_inited(&gb)) { + GB_switch_model_and_reset(&gb, model); + } + else { + GB_init(&gb, model); + + GB_set_boot_rom_load_callback(&gb, load_boot_rom); + GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); + GB_set_pixels_output(&gb, active_pixel_buffer); + GB_set_rgb_encode_callback(&gb, rgb_encode); + GB_set_rumble_callback(&gb, rumble); + GB_set_rumble_mode(&gb, configuration.rumble_mode); + GB_set_sample_rate(&gb, GB_audio_get_frequency()); + GB_set_color_correction_mode(&gb, configuration.color_correction_mode); + update_palette(); + if ((unsigned)configuration.border_mode <= GB_BORDER_ALWAYS) { + GB_set_border_mode(&gb, configuration.border_mode); + } + GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); + GB_set_rewind_length(&gb, configuration.rewind_length); + GB_set_update_input_hint_callback(&gb, handle_events); + GB_apu_set_sample_callback(&gb, gb_audio_callback); + } + + bool error = false; + GB_debugger_clear_symbols(&gb); + start_capturing_logs(); + size_t path_length = strlen(filename); + char extension[4] = {0,}; + if (path_length > 4) { + if (filename[path_length - 4] == '.') { + extension[0] = tolower(filename[path_length - 3]); + extension[1] = tolower(filename[path_length - 2]); + extension[2] = tolower(filename[path_length - 1]); + } + } + if (strcmp(extension, "isx") == 0) { + error = GB_load_isx(&gb, filename); + /* Configure battery */ + char battery_save_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ + replace_extension(filename, path_length, battery_save_path, ".ram"); + battery_save_path_ptr = battery_save_path; + GB_load_battery(&gb, battery_save_path); + } + else { + GB_load_rom(&gb, filename); + } + end_capturing_logs(true, error); + + + /* Configure battery */ + char battery_save_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ + replace_extension(filename, path_length, battery_save_path, ".sav"); + battery_save_path_ptr = battery_save_path; + GB_load_battery(&gb, battery_save_path); + + /* Configure symbols */ + GB_debugger_load_symbol_file(&gb, resource_path("registers.sym")); + + char symbols_path[path_length + 5]; + replace_extension(filename, path_length, symbols_path, ".sym"); + GB_debugger_load_symbol_file(&gb, symbols_path); + + screen_size_changed(); + + /* Run emulation */ + while (true) { + if (paused || rewind_paused) { + SDL_WaitEvent(NULL); + handle_events(&gb); + } + else { + if (do_rewind) { + GB_rewind_pop(&gb); + if (turbo_down) { + GB_rewind_pop(&gb); + } + if (!GB_rewind_pop(&gb)) { + rewind_paused = true; + } + do_rewind = false; + } + GB_run(&gb); + } + + /* These commands can't run in the handle_event function, because they're not safe in a vblank context. */ + if (handle_pending_command()) { + pending_command = GB_SDL_NO_COMMAND; + goto restart; + } + pending_command = GB_SDL_NO_COMMAND; + } +} + +static char prefs_path[1024] = {0, }; + +static void save_configuration(void) +{ + FILE *prefs_file = fopen(prefs_path, "wb"); + if (prefs_file) { + fwrite(&configuration, 1, sizeof(configuration), prefs_file); + fclose(prefs_file); + } +} + +static bool get_arg_flag(const char *flag, int *argc, char **argv) +{ + for (unsigned i = 1; i < *argc; i++) { + if (strcmp(argv[i], flag) == 0) { + (*argc)--; + argv[i] = argv[*argc]; + return true; + } + } + return false; +} + +int main(int argc, char **argv) +{ +#ifdef _WIN32 + SetProcessDPIAware(); +#endif +#define str(x) #x +#define xstr(x) str(x) + fprintf(stderr, "SameBoy v" xstr(VERSION) "\n"); + + bool fullscreen = get_arg_flag("--fullscreen", &argc, argv); + + if (argc > 2) { + fprintf(stderr, "Usage: %s [--fullscreen] [rom]\n", argv[0]); + exit(1); + } + + if (argc == 2) { + filename = argv[1]; + } + + signal(SIGINT, debugger_interrupt); + + SDL_Init(SDL_INIT_EVERYTHING); + + strcpy(prefs_path, resource_path("prefs.bin")); + if (access(prefs_path, R_OK | W_OK) != 0) { + char *prefs_dir = SDL_GetPrefPath("", "SameBoy"); + snprintf(prefs_path, sizeof(prefs_path) - 1, "%sprefs.bin", prefs_dir); + SDL_free(prefs_dir); + } + + FILE *prefs_file = fopen(prefs_path, "rb"); + if (prefs_file) { + fread(&configuration, 1, sizeof(configuration), prefs_file); + fclose(prefs_file); + + /* Sanitize for stability */ + configuration.color_correction_mode %= GB_COLOR_CORRECTION_REDUCE_CONTRAST +1; + configuration.scaling_mode %= GB_SDL_SCALING_MAX; + configuration.default_scale %= GB_SDL_DEFAULT_SCALE_MAX + 1; + configuration.blending_mode %= GB_FRAME_BLENDING_MODE_ACCURATE + 1; + configuration.highpass_mode %= GB_HIGHPASS_MAX; + configuration.model %= MODEL_MAX; + configuration.sgb_revision %= SGB_MAX; + configuration.dmg_palette %= 3; + configuration.border_mode %= GB_BORDER_ALWAYS + 1; + configuration.rumble_mode %= GB_RUMBLE_ALL_GAMES + 1; + } + + if (configuration.model >= MODEL_MAX) { + configuration.model = MODEL_CGB; + } + + if (configuration.default_scale == 0) { + configuration.default_scale = 2; + } + + atexit(save_configuration); + + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + + window = SDL_CreateWindow("SameBoy v" xstr(VERSION), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + 160 * configuration.default_scale, 144 * configuration.default_scale, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + SDL_SetWindowMinimumSize(window, 160, 144); + + if (fullscreen) { + SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); + } + + SDL_GLContext gl_context = SDL_GL_CreateContext(window); + + GLint major = 0, minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + + if (major * 0x100 + minor < 0x302) { + SDL_GL_DeleteContext(gl_context); + gl_context = NULL; + } + + if (gl_context == NULL) { + renderer = SDL_CreateRenderer(window, -1, 0); + texture = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_STREAMING, 160, 144); + pixel_format = SDL_AllocFormat(SDL_GetWindowPixelFormat(window)); + } + else { + pixel_format = SDL_AllocFormat(SDL_PIXELFORMAT_ABGR8888); + } + + GB_audio_init(); + + SDL_EventState(SDL_DROPFILE, SDL_ENABLE); + + if (!init_shader_with_name(&shader, configuration.filter)) { + init_shader_with_name(&shader, "NearestNeighbor"); + } + update_viewport(); + + if (filename == NULL) { + run_gui(false); + } + else { + connect_joypad(); + } + GB_audio_set_paused(false); + run(); // Never returns + return 0; +} diff --git a/bsnes/gb/SDL/opengl_compat.c b/bsnes/gb/SDL/opengl_compat.c new file mode 100644 index 00000000..af7ce6d7 --- /dev/null +++ b/bsnes/gb/SDL/opengl_compat.c @@ -0,0 +1,34 @@ +#define GL_GLEXT_PROTOTYPES +#include + +#ifndef __APPLE__ +#define GL_COMPAT_NAME(func) gl_compat_##func +#define GL_COMPAT_VAR(func) typeof(func) *GL_COMPAT_NAME(func) + +GL_COMPAT_VAR(glCreateShader); +GL_COMPAT_VAR(glGetAttribLocation); +GL_COMPAT_VAR(glGetUniformLocation); +GL_COMPAT_VAR(glUseProgram); +GL_COMPAT_VAR(glGenVertexArrays); +GL_COMPAT_VAR(glBindVertexArray); +GL_COMPAT_VAR(glGenBuffers); +GL_COMPAT_VAR(glBindBuffer); +GL_COMPAT_VAR(glBufferData); +GL_COMPAT_VAR(glEnableVertexAttribArray); +GL_COMPAT_VAR(glVertexAttribPointer); +GL_COMPAT_VAR(glCreateProgram); +GL_COMPAT_VAR(glAttachShader); +GL_COMPAT_VAR(glLinkProgram); +GL_COMPAT_VAR(glGetProgramiv); +GL_COMPAT_VAR(glGetProgramInfoLog); +GL_COMPAT_VAR(glDeleteShader); +GL_COMPAT_VAR(glUniform2f); +GL_COMPAT_VAR(glActiveTexture); +GL_COMPAT_VAR(glUniform1i); +GL_COMPAT_VAR(glBindFragDataLocation); +GL_COMPAT_VAR(glDeleteProgram); +GL_COMPAT_VAR(glShaderSource); +GL_COMPAT_VAR(glCompileShader); +GL_COMPAT_VAR(glGetShaderiv); +GL_COMPAT_VAR(glGetShaderInfoLog); +#endif diff --git a/bsnes/gb/SDL/opengl_compat.h b/bsnes/gb/SDL/opengl_compat.h new file mode 100644 index 00000000..4b79b0c7 --- /dev/null +++ b/bsnes/gb/SDL/opengl_compat.h @@ -0,0 +1,45 @@ +#ifndef opengl_compat_h +#define opengl_compat_h + +#define GL_GLEXT_PROTOTYPES +#include +#include + +#ifndef __APPLE__ +#define GL_COMPAT_NAME(func) gl_compat_##func + +#define GL_COMPAT_WRAPPER(func) \ +({ extern typeof(func) *GL_COMPAT_NAME(func); \ +if (!GL_COMPAT_NAME(func)) GL_COMPAT_NAME(func) = SDL_GL_GetProcAddress(#func); \ + GL_COMPAT_NAME(func); \ +}) + +#define glCreateShader GL_COMPAT_WRAPPER(glCreateShader) +#define glGetAttribLocation GL_COMPAT_WRAPPER(glGetAttribLocation) +#define glGetUniformLocation GL_COMPAT_WRAPPER(glGetUniformLocation) +#define glUseProgram GL_COMPAT_WRAPPER(glUseProgram) +#define glGenVertexArrays GL_COMPAT_WRAPPER(glGenVertexArrays) +#define glBindVertexArray GL_COMPAT_WRAPPER(glBindVertexArray) +#define glGenBuffers GL_COMPAT_WRAPPER(glGenBuffers) +#define glBindBuffer GL_COMPAT_WRAPPER(glBindBuffer) +#define glBufferData GL_COMPAT_WRAPPER(glBufferData) +#define glEnableVertexAttribArray GL_COMPAT_WRAPPER(glEnableVertexAttribArray) +#define glVertexAttribPointer GL_COMPAT_WRAPPER(glVertexAttribPointer) +#define glCreateProgram GL_COMPAT_WRAPPER(glCreateProgram) +#define glAttachShader GL_COMPAT_WRAPPER(glAttachShader) +#define glLinkProgram GL_COMPAT_WRAPPER(glLinkProgram) +#define glGetProgramiv GL_COMPAT_WRAPPER(glGetProgramiv) +#define glGetProgramInfoLog GL_COMPAT_WRAPPER(glGetProgramInfoLog) +#define glDeleteShader GL_COMPAT_WRAPPER(glDeleteShader) +#define glUniform2f GL_COMPAT_WRAPPER(glUniform2f) +#define glActiveTexture GL_COMPAT_WRAPPER(glActiveTexture) +#define glUniform1i GL_COMPAT_WRAPPER(glUniform1i) +#define glBindFragDataLocation GL_COMPAT_WRAPPER(glBindFragDataLocation) +#define glDeleteProgram GL_COMPAT_WRAPPER(glDeleteProgram) +#define glShaderSource GL_COMPAT_WRAPPER(glShaderSource) +#define glCompileShader GL_COMPAT_WRAPPER(glCompileShader) +#define glGetShaderiv GL_COMPAT_WRAPPER(glGetShaderiv) +#define glGetShaderInfoLog GL_COMPAT_WRAPPER(glGetShaderInfoLog) +#endif + +#endif /* opengl_compat_h */ diff --git a/bsnes/gb/SDL/shader.c b/bsnes/gb/SDL/shader.c new file mode 100644 index 00000000..de2ba564 --- /dev/null +++ b/bsnes/gb/SDL/shader.c @@ -0,0 +1,202 @@ +#include +#include +#include "shader.h" +#include "utils.h" + +static const char *vertex_shader = "\n\ +#version 150 \n\ +in vec4 aPosition;\n\ +void main(void) {\n\ +gl_Position = aPosition;\n\ +}\n\ +"; + +static GLuint create_shader(const char *source, GLenum type) +{ + // Create the shader object + GLuint shader = glCreateShader(type); + // Load the shader source + glShaderSource(shader, 1, &source, 0); + // Compile the shader + glCompileShader(shader); + // Check for errors + GLint status = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (status == GL_FALSE) { + GLchar messages[1024]; + glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]); + fprintf(stderr, "GLSL Shader Error: %s", messages); + } + return shader; +} + +static GLuint create_program(const char *vsh, const char *fsh) +{ + // Build shaders + GLuint vertex_shader = create_shader(vsh, GL_VERTEX_SHADER); + GLuint fragment_shader = create_shader(fsh, GL_FRAGMENT_SHADER); + + // Create program + GLuint program = glCreateProgram(); + + // Attach shaders + glAttachShader(program, vertex_shader); + glAttachShader(program, fragment_shader); + + // Link program + glLinkProgram(program); + // Check for errors + GLint status; + glGetProgramiv(program, GL_LINK_STATUS, &status); + + if (status == GL_FALSE) { + GLchar messages[1024]; + glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]); + fprintf(stderr, "GLSL Program Error: %s", messages); + } + + // Delete shaders + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + return program; +} + +bool init_shader_with_name(shader_t *shader, const char *name) +{ + GLint major = 0, minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + + if (major * 0x100 + minor < 0x302) { + return false; + } + + static char master_shader_code[0x801] = {0,}; + static char shader_code[0x10001] = {0,}; + static char final_shader_code[0x10801] = {0,}; + static ssize_t filter_token_location = 0; + + if (!master_shader_code[0]) { + FILE *master_shader_f = fopen(resource_path("Shaders/MasterShader.fsh"), "r"); + if (!master_shader_f) return false; + fread(master_shader_code, 1, sizeof(master_shader_code) - 1, master_shader_f); + fclose(master_shader_f); + filter_token_location = strstr(master_shader_code, "{filter}") - master_shader_code; + if (filter_token_location < 0) { + master_shader_code[0] = 0; + return false; + } + } + + char shader_path[1024]; + sprintf(shader_path, "Shaders/%s.fsh", name); + + FILE *shader_f = fopen(resource_path(shader_path), "r"); + if (!shader_f) return false; + memset(shader_code, 0, sizeof(shader_code)); + fread(shader_code, 1, sizeof(shader_code) - 1, shader_f); + fclose(shader_f); + + memset(final_shader_code, 0, sizeof(final_shader_code)); + memcpy(final_shader_code, master_shader_code, filter_token_location); + strcpy(final_shader_code + filter_token_location, shader_code); + strcat(final_shader_code + filter_token_location, + master_shader_code + filter_token_location + sizeof("{filter}") - 1); + + shader->program = create_program(vertex_shader, final_shader_code); + + // Attributes + shader->position_attribute = glGetAttribLocation(shader->program, "aPosition"); + // Uniforms + shader->resolution_uniform = glGetUniformLocation(shader->program, "output_resolution"); + shader->origin_uniform = glGetUniformLocation(shader->program, "origin"); + + glGenTextures(1, &shader->texture); + glBindTexture(GL_TEXTURE_2D, shader->texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + shader->texture_uniform = glGetUniformLocation(shader->program, "image"); + + glGenTextures(1, &shader->previous_texture); + glBindTexture(GL_TEXTURE_2D, shader->previous_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + shader->previous_texture_uniform = glGetUniformLocation(shader->program, "previous_image"); + + shader->blending_mode_uniform = glGetUniformLocation(shader->program, "frame_blending_mode"); + + // Program + + glUseProgram(shader->program); + + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + GLuint vbo; + glGenBuffers(1, &vbo); + + // Attributes + + + static GLfloat const quad[16] = { + -1.f, -1.f, 0, 1, + -1.f, +1.f, 0, 1, + +1.f, -1.f, 0, 1, + +1.f, +1.f, 0, 1, + }; + + + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW); + glEnableVertexAttribArray(shader->position_attribute); + glVertexAttribPointer(shader->position_attribute, 4, GL_FLOAT, GL_FALSE, 0, 0); + + return true; +} + +void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous, + unsigned source_width, unsigned source_height, + unsigned x, unsigned y, unsigned w, unsigned h, + GB_frame_blending_mode_t blending_mode) +{ + glUseProgram(shader->program); + glUniform2f(shader->origin_uniform, x, y); + glUniform2f(shader->resolution_uniform, w, h); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, shader->texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, source_width, source_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitmap); + glUniform1i(shader->texture_uniform, 0); + glUniform1i(shader->blending_mode_uniform, previous? blending_mode : GB_FRAME_BLENDING_MODE_DISABLED); + if (previous) { + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, shader->previous_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, source_width, source_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, previous); + glUniform1i(shader->previous_texture_uniform, 1); + } + glBindFragDataLocation(shader->program, 0, "frag_color"); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); +} + +void free_shader(shader_t *shader) +{ + GLint major = 0, minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + + if (major * 0x100 + minor < 0x302) { + return; + } + + glDeleteProgram(shader->program); + glDeleteTextures(1, &shader->texture); + glDeleteTextures(1, &shader->previous_texture); + +} diff --git a/bsnes/gb/SDL/shader.h b/bsnes/gb/SDL/shader.h new file mode 100644 index 00000000..149958d5 --- /dev/null +++ b/bsnes/gb/SDL/shader.h @@ -0,0 +1,34 @@ +#ifndef shader_h +#define shader_h +#include "opengl_compat.h" +#include + +typedef struct shader_s { + GLuint resolution_uniform; + GLuint origin_uniform; + GLuint texture_uniform; + GLuint previous_texture_uniform; + GLuint blending_mode_uniform; + + GLuint position_attribute; + GLuint texture; + GLuint previous_texture; + GLuint program; +} shader_t; + +typedef enum { + GB_FRAME_BLENDING_MODE_DISABLED, + GB_FRAME_BLENDING_MODE_SIMPLE, + GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_EVEN = GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_ODD, +} GB_frame_blending_mode_t; + +bool init_shader_with_name(shader_t *shader, const char *name); +void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous, + unsigned source_width, unsigned source_height, + unsigned x, unsigned y, unsigned w, unsigned h, + GB_frame_blending_mode_t blending_mode); +void free_shader(struct shader_s *shader); + +#endif /* shader_h */ diff --git a/bsnes/gb/SDL/utils.c b/bsnes/gb/SDL/utils.c new file mode 100644 index 00000000..8cdd00b9 --- /dev/null +++ b/bsnes/gb/SDL/utils.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include "utils.h" + +const char *resource_folder(void) +{ +#ifdef DATA_DIR + return DATA_DIR; +#else + static const char *ret = NULL; + if (!ret) { + ret = SDL_GetBasePath(); + if (!ret) { + ret = "./"; + } + } + return ret; +#endif +} + +char *resource_path(const char *filename) +{ + static char path[1024]; + snprintf(path, sizeof(path), "%s%s", resource_folder(), filename); + return path; +} + + +void replace_extension(const char *src, size_t length, char *dest, const char *ext) +{ + memcpy(dest, src, length); + dest[length] = 0; + + /* Remove extension */ + for (size_t i = length; i--;) { + if (dest[i] == '/') break; + if (dest[i] == '.') { + dest[i] = 0; + break; + } + } + + /* Add new extension */ + strcat(dest, ext); +} diff --git a/bsnes/gb/SDL/utils.h b/bsnes/gb/SDL/utils.h new file mode 100644 index 00000000..216e723e --- /dev/null +++ b/bsnes/gb/SDL/utils.h @@ -0,0 +1,9 @@ +#ifndef utils_h +#define utils_h +#include + +const char *resource_folder(void); +char *resource_path(const char *filename); +void replace_extension(const char *src, size_t length, char *dest, const char *ext); + +#endif /* utils_h */ diff --git a/bsnes/gb/Shaders/AAOmniScaleLegacy.fsh b/bsnes/gb/Shaders/AAOmniScaleLegacy.fsh new file mode 100644 index 00000000..b84e2ced --- /dev/null +++ b/bsnes/gb/Shaders/AAOmniScaleLegacy.fsh @@ -0,0 +1,118 @@ + +STATIC float quickDistance(vec4 a, vec4 b) +{ + return abs(a.x - b.x) + abs(a.y - b.y) + abs(a.z - b.z); +} + +STATIC vec4 omniScale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pixel = position * input_resolution - vec2(0.5, 0.5); + + vec4 q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + vec4 q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + vec4 q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + vec4 q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + vec2 pos = fract(pixel); + + /* Special handling for diaonals */ + bool hasDownDiagonal = false; + bool hasUpDiagonal = false; + if (equal(q12, q21) && inequal(q11, q22)) hasUpDiagonal = true; + else if (inequal(q12, q21) && equal(q11, q22)) hasDownDiagonal = true; + else if (equal(q12, q21) && equal(q11, q22)) { + if (equal(q11, q12)) return q11; + int diagonalBias = 0; + for (float y = -1.0; y < 3.0; y++) { + for (float x = -1.0; x < 3.0; x++) { + vec4 color = texture(image, (pixel + vec2(x, y)) / input_resolution); + if (equal(color, q11)) diagonalBias++; + if (equal(color, q12)) diagonalBias--; + } + } + if (diagonalBias <= 0) { + hasDownDiagonal = true; + } + if (diagonalBias >= 0) { + hasUpDiagonal = true; + } + } + + if (hasUpDiagonal || hasDownDiagonal) { + vec4 downDiagonalResult, upDiagonalResult; + + if (hasUpDiagonal) { + float diagonalPos = pos.x + pos.y; + + if (diagonalPos < 0.5) { + upDiagonalResult = q11; + } + else if (diagonalPos > 1.5) { + upDiagonalResult = q22; + } + else { + upDiagonalResult = q12; + } + } + + if (hasDownDiagonal) { + float diagonalPos = 1.0 - pos.x + pos.y; + + if (diagonalPos < 0.5) { + downDiagonalResult = q21; + } + else if (diagonalPos > 1.5) { + downDiagonalResult = q12; + } + else { + downDiagonalResult = q11; + } + } + + if (!hasUpDiagonal) return downDiagonalResult; + if (!hasDownDiagonal) return upDiagonalResult; + return mix(downDiagonalResult, upDiagonalResult, 0.5); + } + + vec4 r1 = mix(q11, q21, fract(pos.x)); + vec4 r2 = mix(q12, q22, fract(pos.x)); + + vec4 unqunatized = mix(r1, r2, fract(pos.y)); + + float q11d = quickDistance(unqunatized, q11); + float q21d = quickDistance(unqunatized, q21); + float q12d = quickDistance(unqunatized, q12); + float q22d = quickDistance(unqunatized, q22); + + float best = min(q11d, + min(q21d, + min(q12d, + q22d))); + + if (equal(q11d, best)) { + return q11; + } + + if (equal(q21d, best)) { + return q21; + } + + if (equal(q12d, best)) { + return q12; + } + + return q22; +} + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pixel = vec2(1.0, 1.0) / output_resolution; + // 4-pixel super sampling + + vec4 q11 = omniScale(image, position + pixel * vec2(-0.25, -0.25), input_resolution, output_resolution); + vec4 q21 = omniScale(image, position + pixel * vec2(+0.25, -0.25), input_resolution, output_resolution); + vec4 q12 = omniScale(image, position + pixel * vec2(-0.25, +0.25), input_resolution, output_resolution); + vec4 q22 = omniScale(image, position + pixel * vec2(+0.25, +0.25), input_resolution, output_resolution); + + return (q11 + q21 + q12 + q22) / 4.0; +} diff --git a/bsnes/gb/Shaders/AAScale2x.fsh b/bsnes/gb/Shaders/AAScale2x.fsh new file mode 100644 index 00000000..d51a9a6a --- /dev/null +++ b/bsnes/gb/Shaders/AAScale2x.fsh @@ -0,0 +1,44 @@ +STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / input_resolution; + + // texel arrangement + // A B C + // D E F + // G H I + // vec4 A = texture(image, position + vec2( -o.x, o.y)); + vec4 B = texture(image, position + vec2( 0, o.y)); + // vec4 C = texture(image, position + vec2( o.x, o.y)); + vec4 D = texture(image, position + vec2( -o.x, 0)); + vec4 E = texture(image, position + vec2( 0, 0)); + vec4 F = texture(image, position + vec2( o.x, 0)); + // vec4 G = texture(image, position + vec2( -o.x, -o.y)); + vec4 H = texture(image, position + vec2( 0, -o.y)); + // vec4 I = texture(image, position + vec2( o.x, -o.y)); + vec2 p = position * input_resolution; + // p = the position within a pixel [0...1] + p = fract(p); + if (p.x > .5) { + if (p.y > .5) { + // Top Right + return equal(B, F) && inequal(B, D) && inequal(F, H) ? F : E; + } else { + // Bottom Right + return equal(H, F) && inequal(D, H) && inequal(B, F) ? F : E; + } + } else { + if (p.y > .5) { + // Top Left + return equal(D, B) && inequal(B, F) && inequal(D, H) ? D : E; + } else { + // Bottom Left + return equal(D, H) && inequal(D, B) && inequal(H, F) ? D : E; + } + } +} + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + return mix(texture(image, position), scale2x(image, position, input_resolution, output_resolution), 0.5); +} diff --git a/bsnes/gb/Shaders/AAScale4x.fsh b/bsnes/gb/Shaders/AAScale4x.fsh new file mode 100644 index 00000000..b59b80e9 --- /dev/null +++ b/bsnes/gb/Shaders/AAScale4x.fsh @@ -0,0 +1,86 @@ +STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / input_resolution; + // texel arrangement + // A B C + // D E F + // G H I + // vec4 A = texture(image, position + vec2( -o.x, o.y)); + vec4 B = texture(image, position + vec2( 0, o.y)); + // vec4 C = texture(image, position + vec2( o.x, o.y)); + vec4 D = texture(image, position + vec2( -o.x, 0)); + vec4 E = texture(image, position + vec2( 0, 0)); + vec4 F = texture(image, position + vec2( o.x, 0)); + // vec4 G = texture(image, position + vec2( -o.x, -o.y)); + vec4 H = texture(image, position + vec2( 0, -o.y)); + // vec4 I = texture(image, position + vec2( o.x, -o.y)); + vec2 p = position * input_resolution; + // p = the position within a pixel [0...1] + p = fract(p); + if (p.x > .5) { + if (p.y > .5) { + // Top Right + return equal(B, F) && inequal(B, D) && inequal(F, H) ? F : E; + } else { + // Bottom Right + return equal(H, F) && inequal(D, H) && inequal(B, F) ? F : E; + } + } else { + if (p.y > .5) { + // Top Left + return equal(D, B) && inequal(B, F) && inequal(D, H) ? D : E; + } else { + // Bottom Left + return equal(D, H) && inequal(D, B) && inequal(H, F) ? D : E; + } + } +} + +STATIC vec4 aaScale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + return mix(texture(image, position), scale2x(image, position, input_resolution, output_resolution), 0.5); +} + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / (input_resolution * 2.); + + // texel arrangement + // A B C + // D E F + // G H I + // vec4 A = aaScale2x(image, position + vec2( -o.x, o.y), input_resolution, output_resolution); + vec4 B = aaScale2x(image, position + vec2( 0, o.y), input_resolution, output_resolution); + // vec4 C = aaScale2x(image, position + vec2( o.x, o.y), input_resolution, output_resolution); + vec4 D = aaScale2x(image, position + vec2( -o.x, 0), input_resolution, output_resolution); + vec4 E = aaScale2x(image, position + vec2( 0, 0), input_resolution, output_resolution); + vec4 F = aaScale2x(image, position + vec2( o.x, 0), input_resolution, output_resolution); + // vec4 G = aaScale2x(image, position + vec2( -o.x, -o.y), input_resolution, output_resolution); + vec4 H = aaScale2x(image, position + vec2( 0, -o.y), input_resolution, output_resolution); + // vec4 I = aaScale2x(image, position + vec2( o.x, -o.y), input_resolution, output_resolution); + vec4 R; + vec2 p = position * input_resolution * 2.; + // p = the position within a pixel [0...1] + p = fract(p); + if (p.x > .5) { + if (p.y > .5) { + // Top Right + R = equal(B, F) && inequal(B, D) && inequal(F, H) ? F : E; + } else { + // Bottom Right + R = equal(H, F) && inequal(D, H) && inequal(B, F) ? F : E; + } + } else { + if (p.y > .5) { + // Top Left + R = equal(D, B) && inequal(B, F) && inequal(D, H) ? D : E; + } else { + // Bottom Left + R = equal(D, H) && inequal(D, B) && inequal(H, F) ? D : E; + } + } + + return mix(R, E, 0.5); +} diff --git a/bsnes/gb/Shaders/Bilinear.fsh b/bsnes/gb/Shaders/Bilinear.fsh new file mode 100644 index 00000000..e68e1d19 --- /dev/null +++ b/bsnes/gb/Shaders/Bilinear.fsh @@ -0,0 +1,14 @@ +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pixel = position * input_resolution - vec2(0.5, 0.5); + + vec4 q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + vec4 q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + vec4 q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + vec4 q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + vec4 r1 = mix(q11, q21, fract(pixel.x)); + vec4 r2 = mix(q12, q22, fract(pixel.x)); + + return mix (r1, r2, fract(pixel.y)); +} diff --git a/bsnes/gb/Shaders/CRT.fsh b/bsnes/gb/Shaders/CRT.fsh new file mode 100644 index 00000000..4cbab721 --- /dev/null +++ b/bsnes/gb/Shaders/CRT.fsh @@ -0,0 +1,162 @@ +#define COLOR_LOW 0.7 +#define COLOR_HIGH 1.0 +#define VERTICAL_BORDER_DEPTH 0.6 +#define SCANLINE_DEPTH 0.3 +#define CURVENESS 0.3 + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + /* Curve and pixel ratio */ + float y_curve = cos(position.x - 0.5) * CURVENESS + (1 - CURVENESS); + float y_multiplier = 8.0 / 7.0 / y_curve; + position.y *= y_multiplier; + position.y -= (y_multiplier - 1) / 2; + if (position.y < 0.0) return vec4(0,0,0,0); + if (position.y > 1.0) return vec4(0,0,0,0); + + float x_curve = cos(position.y - 0.5) * CURVENESS + (1 - CURVENESS); + float x_multiplier = 1/x_curve; + position.x *= x_multiplier; + position.x -= (x_multiplier - 1) / 2; + if (position.x < 0.0) return vec4(0,0,0,0); + if (position.x > 1.0) return vec4(0,0,0,0); + + /* Setting up common vars */ + vec2 pos = fract(position * input_resolution); + vec2 sub_pos = fract(position * input_resolution * 6); + + vec4 center = texture(image, position); + vec4 left = texture(image, position - vec2(1.0 / input_resolution.x, 0)); + vec4 right = texture(image, position + vec2(1.0 / input_resolution.x, 0)); + + /* Vertical blurring */ + if (pos.y < 1.0 / 6.0) { + center = mix(center, texture(image, position + vec2(0, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); + left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); + right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); + } + else if (pos.y > 5.0 / 6.0) { + center = mix(center, texture(image, position + vec2(0, 1.0 / input_resolution.y)), sub_pos.y / 2.0); + left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); + right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); + } + + /* Scanlines */ + float scanline_multiplier; + if (pos.y < 0.5) { + scanline_multiplier = (pos.y * 2) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + else { + scanline_multiplier = ((1 - pos.y) * 2) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + + center *= scanline_multiplier; + left *= scanline_multiplier; + right *= scanline_multiplier; + + /* Vertical seperator for shadow masks */ + bool odd = bool(int((position * input_resolution).x) & 1); + if (odd) { + pos.y += 0.5; + pos.y = fract(pos.y); + } + + if (pos.y < 1.0 / 3.0) { + float gradient_position = pos.y * 3.0; + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + } + else if (pos.y > 2.0 / 3.0) { + float gradient_position = (1 - pos.y) * 3.0; + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + } + + /* Blur the edges of the separators of adjacent columns */ + if (pos.x < 1.0 / 6.0 || pos.x > 5.0 / 6.0) { + pos.y += 0.5; + pos.y = fract(pos.y); + + if (pos.y < 1.0 / 3.0) { + float gradient_position = pos.y * 3.0; + if (pos.x < 0.5) { + gradient_position = 1 - (1 - gradient_position) * (1 - (pos.x) * 6.0); + } + else { + gradient_position = 1 - (1 - gradient_position) * (1 - (1 - pos.x) * 6.0); + } + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + } + else if (pos.y > 2.0 / 3.0) { + float gradient_position = (1 - pos.y) * 3.0; + if (pos.x < 0.5) { + gradient_position = 1 - (1 - gradient_position) * (1 - (pos.x) * 6.0); + } + else { + gradient_position = 1 - (1 - gradient_position) * (1 - (1 - pos.x) * 6.0); + } + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + } + } + + + /* Subpixel blurring, like LCD filter*/ + + vec4 midleft = mix(left, center, 0.5); + vec4 midright = mix(right, center, 0.5); + + vec4 ret; + if (pos.x < 1.0 / 6.0) { + ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_HIGH * left.b, 1), + vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), + sub_pos.x); + } + else if (pos.x < 2.0 / 6.0) { + ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), + vec4(COLOR_HIGH * center.r, COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), + sub_pos.x); + } + else if (pos.x < 3.0 / 6.0) { + ret = mix(vec4(COLOR_HIGH * center.r , COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), + vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g, COLOR_LOW * center.b, 1), + sub_pos.x); + } + else if (pos.x < 4.0 / 6.0) { + ret = mix(vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g , COLOR_LOW * center.b, 1), + vec4(COLOR_LOW * right.r , COLOR_HIGH * center.g, COLOR_HIGH * center.b, 1), + sub_pos.x); + } + else if (pos.x < 5.0 / 6.0) { + ret = mix(vec4(COLOR_LOW * right.r, COLOR_HIGH * center.g , COLOR_HIGH * center.b, 1), + vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), + sub_pos.x); + } + else { + ret = mix(vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), + vec4(COLOR_HIGH * right.r, COLOR_LOW * right.g , COLOR_HIGH * center.b, 1), + sub_pos.x); + } + + /* Anti alias the curve */ + vec2 pixel_position = position * output_resolution; + if (pixel_position.x < 1) { + ret *= pixel_position.x; + } + else if (pixel_position.x > output_resolution.x - 1) { + ret *= output_resolution.x - pixel_position.x; + } + if (pixel_position.y < 1) { + ret *= pixel_position.y; + } + else if (pixel_position.y > output_resolution.y - 1) { + ret *= output_resolution.y - pixel_position.y; + } + + return ret; +} diff --git a/bsnes/gb/Shaders/HQ2x.fsh b/bsnes/gb/Shaders/HQ2x.fsh new file mode 100644 index 00000000..7ae80637 --- /dev/null +++ b/bsnes/gb/Shaders/HQ2x.fsh @@ -0,0 +1,115 @@ +/* Based on this (really good) article: http://blog.pkh.me/p/19-butchering-hqx-scaling-filters.html */ + +/* The colorspace used by the HQnx filters is not really YUV, despite the algorithm description claims it is. It is + also not normalized. Therefore, we shall call the colorspace used by HQnx "HQ Colorspace" to avoid confusion. */ +STATIC vec3 rgb_to_hq_colospace(vec4 rgb) +{ + return vec3( 0.250 * rgb.r + 0.250 * rgb.g + 0.250 * rgb.b, + 0.250 * rgb.r - 0.000 * rgb.g - 0.250 * rgb.b, + -0.125 * rgb.r + 0.250 * rgb.g - 0.125 * rgb.b); +} + +STATIC bool is_different(vec4 a, vec4 b) +{ + vec3 diff = abs(rgb_to_hq_colospace(a) - rgb_to_hq_colospace(b)); + return diff.x > 0.018 || diff.y > 0.002 || diff.z > 0.005; +} + +#define P(m, r) ((pattern & (m)) == (r)) + +STATIC vec4 interp_2px(vec4 c1, float w1, vec4 c2, float w2) +{ + return (c1 * w1 + c2 * w2) / (w1 + w2); +} + +STATIC vec4 interp_3px(vec4 c1, float w1, vec4 c2, float w2, vec4 c3, float w3) +{ + return (c1 * w1 + c2 * w2 + c3 * w3) / (w1 + w2 + w3); +} + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / input_resolution; + + /* We always calculate the top left pixel. If we need a different pixel, we flip the image */ + + // p = the position within a pixel [0...1] + vec2 p = fract(position * input_resolution); + + if (p.x > 0.5) o.x = -o.x; + if (p.y > 0.5) o.y = -o.y; + + + + vec4 w0 = texture(image, position + vec2( -o.x, -o.y)); + vec4 w1 = texture(image, position + vec2( 0, -o.y)); + vec4 w2 = texture(image, position + vec2( o.x, -o.y)); + vec4 w3 = texture(image, position + vec2( -o.x, 0)); + vec4 w4 = texture(image, position + vec2( 0, 0)); + vec4 w5 = texture(image, position + vec2( o.x, 0)); + vec4 w6 = texture(image, position + vec2( -o.x, o.y)); + vec4 w7 = texture(image, position + vec2( 0, o.y)); + vec4 w8 = texture(image, position + vec2( o.x, o.y)); + + int pattern = 0; + if (is_different(w0, w4)) pattern |= 1; + if (is_different(w1, w4)) pattern |= 2; + if (is_different(w2, w4)) pattern |= 4; + if (is_different(w3, w4)) pattern |= 8; + if (is_different(w5, w4)) pattern |= 16; + if (is_different(w6, w4)) pattern |= 32; + if (is_different(w7, w4)) pattern |= 64; + if (is_different(w8, w4)) pattern |= 128; + + if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) { + return interp_2px(w4, 3.0, w3, 1.0); + } + if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) { + return interp_2px(w4, 3.0, w1, 1.0); + } + if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) { + return w4; + } + if ((P(0x6f,0x2a) || P(0x5b,0x0a) || P(0xbf,0x3a) || P(0xdf,0x5a) || + P(0x9f,0x8a) || P(0xcf,0x8a) || P(0xef,0x4e) || P(0x3f,0x0e) || + P(0xfb,0x5a) || P(0xbb,0x8a) || P(0x7f,0x5a) || P(0xaf,0x8a) || + P(0xeb,0x8a)) && is_different(w3, w1)) { + return interp_2px(w4, 3.0, w0, 1.0); + } + if (P(0x0b,0x08)) { + return interp_3px(w4, 2.0, w0, 1.0, w1, 1.0); + } + if (P(0x0b,0x02)) { + return interp_3px(w4, 2.0, w0, 1.0, w3, 1.0); + } + if (P(0x2f,0x2f)) { + return interp_3px(w4, 4.0, w3, 1.0, w1, 1.0); + } + if (P(0xbf,0x37) || P(0xdb,0x13)) { + return interp_3px(w4, 5.0, w1, 2.0, w3, 1.0); + } + if (P(0xdb,0x49) || P(0xef,0x6d)) { + return interp_3px(w4, 5.0, w3, 2.0, w1, 1.0); + } + if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) { + return interp_2px(w4, 3.0, w3, 1.0); + } + if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) { + return interp_2px(w4, 3.0, w1, 1.0); + } + if (P(0x7e,0x2a) || P(0xef,0xab) || P(0xbf,0x8f) || P(0x7e,0x0e)) { + return interp_3px(w4, 2.0, w3, 3.0, w1, 3.0); + } + if (P(0xfb,0x6a) || P(0x6f,0x6e) || P(0x3f,0x3e) || P(0xfb,0xfa) || + P(0xdf,0xde) || P(0xdf,0x1e)) { + return interp_2px(w4, 3.0, w0, 1.0); + } + if (P(0x0a,0x00) || P(0x4f,0x4b) || P(0x9f,0x1b) || P(0x2f,0x0b) || + P(0xbe,0x0a) || P(0xee,0x0a) || P(0x7e,0x0a) || P(0xeb,0x4b) || + P(0x3b,0x1b)) { + return interp_3px(w4, 2.0, w3, 1.0, w1, 1.0); + } + + return interp_3px(w4, 6.0, w3, 1.0, w1, 1.0); +} diff --git a/bsnes/gb/Shaders/LCD.fsh b/bsnes/gb/Shaders/LCD.fsh new file mode 100644 index 00000000..d20a7c93 --- /dev/null +++ b/bsnes/gb/Shaders/LCD.fsh @@ -0,0 +1,68 @@ +#define COLOR_LOW 0.8 +#define COLOR_HIGH 1.0 +#define SCANLINE_DEPTH 0.1 + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pos = fract(position * input_resolution); + vec2 sub_pos = fract(position * input_resolution * 6); + + vec4 center = texture(image, position); + vec4 left = texture(image, position - vec2(1.0 / input_resolution.x, 0)); + vec4 right = texture(image, position + vec2(1.0 / input_resolution.x, 0)); + + if (pos.y < 1.0 / 6.0) { + center = mix(center, texture(image, position + vec2(0, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); + left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); + right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); + center *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + left *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + right *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + else if (pos.y > 5.0 / 6.0) { + center = mix(center, texture(image, position + vec2(0, 1.0 / input_resolution.y)), sub_pos.y / 2.0); + left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); + right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); + center *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + left *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + right *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + + + vec4 midleft = mix(left, center, 0.5); + vec4 midright = mix(right, center, 0.5); + + vec4 ret; + if (pos.x < 1.0 / 6.0) { + ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_HIGH * left.b, 1), + vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), + sub_pos.x); + } + else if (pos.x < 2.0 / 6.0) { + ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), + vec4(COLOR_HIGH * center.r, COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), + sub_pos.x); + } + else if (pos.x < 3.0 / 6.0) { + ret = mix(vec4(COLOR_HIGH * center.r , COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), + vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g, COLOR_LOW * center.b, 1), + sub_pos.x); + } + else if (pos.x < 4.0 / 6.0) { + ret = mix(vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g , COLOR_LOW * center.b, 1), + vec4(COLOR_LOW * right.r , COLOR_HIGH * center.g, COLOR_HIGH * center.b, 1), + sub_pos.x); + } + else if (pos.x < 5.0 / 6.0) { + ret = mix(vec4(COLOR_LOW * right.r, COLOR_HIGH * center.g , COLOR_HIGH * center.b, 1), + vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), + sub_pos.x); + } + else { + ret = mix(vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), + vec4(COLOR_HIGH * right.r, COLOR_LOW * right.g , COLOR_HIGH * center.b, 1), + sub_pos.x); + } + + return ret; +} diff --git a/bsnes/gb/Shaders/MasterShader.fsh b/bsnes/gb/Shaders/MasterShader.fsh new file mode 100644 index 00000000..3f891d5d --- /dev/null +++ b/bsnes/gb/Shaders/MasterShader.fsh @@ -0,0 +1,72 @@ +#version 150 +uniform sampler2D image; +uniform sampler2D previous_image; +uniform int frame_blending_mode; + +uniform vec2 output_resolution; +uniform vec2 origin; + +#define equal(x, y) ((x) == (y)) +#define inequal(x, y) ((x) != (y)) +#define STATIC +#define GAMMA (2.2) + +out vec4 frag_color; + +vec4 _texture(sampler2D t, vec2 pos) +{ + return pow(texture(t, pos), vec4(GAMMA)); +} + +#define texture _texture + +#line 1 +{filter} + + +#define BLEND_BIAS (2.0/5.0) + +#define DISABLED 0 +#define SIMPLE 1 +#define ACCURATE 2 +#define ACCURATE_EVEN ACCURATE +#define ACCURATE_ODD 3 + +void main() +{ + vec2 position = gl_FragCoord.xy - origin; + position /= output_resolution; + position.y = 1 - position.y; + vec2 input_resolution = textureSize(image, 0); + + float ratio; + switch (frame_blending_mode) { + default: + case DISABLED: + frag_color = pow(scale(image, position, input_resolution, output_resolution), vec4(1.0 / GAMMA)); + return; + case SIMPLE: + ratio = 0.5; + break; + case ACCURATE_EVEN: + if ((int(position.y * input_resolution.y) & 1) == 0) { + ratio = BLEND_BIAS; + } + else { + ratio = 1 - BLEND_BIAS; + } + break; + case ACCURATE_ODD: + if ((int(position.y * input_resolution.y) & 1) == 0) { + ratio = 1 - BLEND_BIAS; + } + else { + ratio = BLEND_BIAS; + } + break; + } + + frag_color = pow(mix(scale(image, position, input_resolution, output_resolution), + scale(previous_image, position, input_resolution, output_resolution), ratio), vec4(1.0 / GAMMA)); + +} diff --git a/bsnes/gb/Shaders/MasterShader.metal b/bsnes/gb/Shaders/MasterShader.metal new file mode 100644 index 00000000..b900176f --- /dev/null +++ b/bsnes/gb/Shaders/MasterShader.metal @@ -0,0 +1,94 @@ +#include +#include +#include + +using namespace metal; + +/* For GLSL compatibility */ +typedef float2 vec2; +typedef float3 vec3; +typedef float4 vec4; +typedef texture2d sampler2D; +#define equal(x, y) all((x) == (y)) +#define inequal(x, y) any((x) != (y)) +#define STATIC static +#define GAMMA (2.2) + +typedef struct { + float4 position [[position]]; + float2 texcoords; +} rasterizer_data; + +// Vertex Function +vertex rasterizer_data vertex_shader(uint index [[ vertex_id ]], + constant vector_float2 *vertices [[ buffer(0) ]]) +{ + rasterizer_data out; + + out.position.xy = vertices[index].xy; + out.position.z = 0.0; + out.position.w = 1.0; + out.texcoords = (vertices[index].xy + float2(1, 1)) / 2.0; + + return out; +} + + +static inline float4 texture(texture2d texture, float2 pos) +{ + constexpr sampler texture_sampler; + return pow(float4(texture.sample(texture_sampler, pos)), GAMMA); +} + +#line 1 +{filter} + +#define BLEND_BIAS (2.0/5.0) + +enum frame_blending_mode { + DISABLED, + SIMPLE, + ACCURATE, + ACCURATE_EVEN = ACCURATE, + ACCURATE_ODD, +}; + +fragment float4 fragment_shader(rasterizer_data in [[stage_in]], + texture2d image [[ texture(0) ]], + texture2d previous_image [[ texture(1) ]], + constant enum frame_blending_mode *frame_blending_mode [[ buffer(0) ]], + constant float2 *output_resolution [[ buffer(1) ]]) +{ + float2 input_resolution = float2(image.get_width(), image.get_height()); + + in.texcoords.y = 1 - in.texcoords.y; + float ratio; + switch (*frame_blending_mode) { + default: + case DISABLED: + return scale(image, in.texcoords, input_resolution, *output_resolution); + case SIMPLE: + ratio = 0.5; + break; + case ACCURATE_EVEN: + if (((int)(in.texcoords.y * input_resolution.y) & 1) == 0) { + ratio = BLEND_BIAS; + } + else { + ratio = 1 - BLEND_BIAS; + } + break; + case ACCURATE_ODD: + if (((int)(in.texcoords.y * input_resolution.y) & 1) == 0) { + ratio = 1 - BLEND_BIAS; + } + else { + ratio = BLEND_BIAS; + } + break; + } + + return pow(mix(scale(image, in.texcoords, input_resolution, *output_resolution), + scale(previous_image, in.texcoords, input_resolution, *output_resolution), ratio), 1 / GAMMA); +} + diff --git a/bsnes/gb/Shaders/MonoLCD.fsh b/bsnes/gb/Shaders/MonoLCD.fsh new file mode 100644 index 00000000..009e1db1 --- /dev/null +++ b/bsnes/gb/Shaders/MonoLCD.fsh @@ -0,0 +1,50 @@ +#define SCANLINE_DEPTH 0.25 +#define BLOOM 0.4 + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pixel = position * input_resolution - vec2(0.5, 0.5); + + vec4 q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + vec4 q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + vec4 q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + vec4 q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + vec2 s = smoothstep(0., 1., fract(pixel)); + + vec4 r1 = mix(q11, q21, s.x); + vec4 r2 = mix(q12, q22, s.x); + + vec2 pos = fract(position * input_resolution); + vec2 sub_pos = fract(position * input_resolution * 6); + + float multiplier = 1.0; + + if (pos.y < 1.0 / 6.0) { + multiplier *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + else if (pos.y > 5.0 / 6.0) { + multiplier *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + + if (pos.x < 1.0 / 6.0) { + multiplier *= sub_pos.x * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + else if (pos.x > 5.0 / 6.0) { + multiplier *= (1.0 - sub_pos.x) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + } + + vec4 pre_shadow = mix(texture(image, position) * multiplier, mix(r1, r2, s.y), BLOOM); + pixel += vec2(-0.6, -0.8); + + q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + r1 = mix(q11, q21, fract(pixel.x)); + r2 = mix(q12, q22, fract(pixel.x)); + + vec4 shadow = mix(r1, r2, fract(pixel.y)); + return mix(min(shadow, pre_shadow), pre_shadow, 0.75); +} diff --git a/bsnes/gb/Shaders/NearestNeighbor.fsh b/bsnes/gb/Shaders/NearestNeighbor.fsh new file mode 100644 index 00000000..7f37024c --- /dev/null +++ b/bsnes/gb/Shaders/NearestNeighbor.fsh @@ -0,0 +1,4 @@ +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + return texture(image, position); +} diff --git a/bsnes/gb/Shaders/OmniScale.fsh b/bsnes/gb/Shaders/OmniScale.fsh new file mode 100644 index 00000000..eab27ae8 --- /dev/null +++ b/bsnes/gb/Shaders/OmniScale.fsh @@ -0,0 +1,262 @@ +/* OmniScale is derived from the pattern based design of HQnx, but with the following general differences: + - The actual output calculating was completely redesigned as resolution independent graphic generator. This allows + scaling to any factor. + - HQnx approximations that were good enough for a 2x/3x/4x factor were refined, creating smoother gradients. + - "Quarters" can be interpolated in more ways than in the HQnx filters + - If a pattern does not provide enough information to determine the suitable scaling interpolation, up to 16 pixels + per quarter are sampled (in contrast to the usual 9) in order to determine the best interpolation. + */ + +/* We use the same colorspace as the HQ algorithms. */ +STATIC vec3 rgb_to_hq_colospace(vec4 rgb) +{ + return vec3( 0.250 * rgb.r + 0.250 * rgb.g + 0.250 * rgb.b, + 0.250 * rgb.r - 0.000 * rgb.g - 0.250 * rgb.b, + -0.125 * rgb.r + 0.250 * rgb.g - 0.125 * rgb.b); +} + + +STATIC bool is_different(vec4 a, vec4 b) +{ + vec3 diff = abs(rgb_to_hq_colospace(a) - rgb_to_hq_colospace(b)); + return diff.x > 0.018 || diff.y > 0.002 || diff.z > 0.005; +} + +#define P(m, r) ((pattern & (m)) == (r)) + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / input_resolution; + + /* We always calculate the top left quarter. If we need a different quarter, we flip our co-ordinates */ + + // p = the position within a pixel [0...1] + vec2 p = fract(position * input_resolution); + + if (p.x > 0.5) { + o.x = -o.x; + p.x = 1.0 - p.x; + } + if (p.y > 0.5) { + o.y = -o.y; + p.y = 1.0 - p.y; + } + + vec4 w0 = texture(image, position + vec2( -o.x, -o.y)); + vec4 w1 = texture(image, position + vec2( 0, -o.y)); + vec4 w2 = texture(image, position + vec2( o.x, -o.y)); + vec4 w3 = texture(image, position + vec2( -o.x, 0)); + vec4 w4 = texture(image, position + vec2( 0, 0)); + vec4 w5 = texture(image, position + vec2( o.x, 0)); + vec4 w6 = texture(image, position + vec2( -o.x, o.y)); + vec4 w7 = texture(image, position + vec2( 0, o.y)); + vec4 w8 = texture(image, position + vec2( o.x, o.y)); + + int pattern = 0; + if (is_different(w0, w4)) pattern |= 1 << 0; + if (is_different(w1, w4)) pattern |= 1 << 1; + if (is_different(w2, w4)) pattern |= 1 << 2; + if (is_different(w3, w4)) pattern |= 1 << 3; + if (is_different(w5, w4)) pattern |= 1 << 4; + if (is_different(w6, w4)) pattern |= 1 << 5; + if (is_different(w7, w4)) pattern |= 1 << 6; + if (is_different(w8, w4)) pattern |= 1 << 7; + + if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) { + return mix(w4, w3, 0.5 - p.x); + } + if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) { + return mix(w4, w1, 0.5 - p.y); + } + if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) { + return w4; + } + if ((P(0x6f,0x2a) || P(0x5b,0x0a) || P(0xbf,0x3a) || P(0xdf,0x5a) || + P(0x9f,0x8a) || P(0xcf,0x8a) || P(0xef,0x4e) || P(0x3f,0x0e) || + P(0xfb,0x5a) || P(0xbb,0x8a) || P(0x7f,0x5a) || P(0xaf,0x8a) || + P(0xeb,0x8a)) && is_different(w3, w1)) { + return mix(w4, mix(w4, w0, 0.5 - p.x), 0.5 - p.y); + } + if (P(0x0b,0x08)) { + return mix(mix(w0 * 0.375 + w1 * 0.25 + w4 * 0.375, w4 * 0.5 + w1 * 0.5, p.x * 2.0), w4, p.y * 2.0); + } + if (P(0x0b,0x02)) { + return mix(mix(w0 * 0.375 + w3 * 0.25 + w4 * 0.375, w4 * 0.5 + w3 * 0.5, p.y * 2.0), w4, p.x * 2.0); + } + if (P(0x2f,0x2f)) { + float dist = length(p - vec2(0.5)); + float pixel_size = length(1.0 / (output_resolution / input_resolution)); + if (dist < 0.5 - pixel_size / 2) { + return w4; + } + vec4 r; + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist > 0.5 + pixel_size / 2) { + return r; + } + return mix(w4, r, (dist - 0.5 + pixel_size / 2) / pixel_size); + } + if (P(0xbf,0x37) || P(0xdb,0x13)) { + float dist = p.x - 2.0 * p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + if (dist > pixel_size / 2) { + return w1; + } + vec4 r = mix(w3, w4, p.x + 0.5); + if (dist < -pixel_size / 2) { + return r; + } + return mix(r, w1, (dist + pixel_size / 2) / pixel_size); + } + if (P(0xdb,0x49) || P(0xef,0x6d)) { + float dist = p.y - 2.0 * p.x; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + if (p.y - 2.0 * p.x > pixel_size / 2) { + return w3; + } + vec4 r = mix(w1, w4, p.x + 0.5); + if (dist < -pixel_size / 2) { + return r; + } + return mix(r, w3, (dist + pixel_size / 2) / pixel_size); + } + if (P(0xbf,0x8f) || P(0x7e,0x0e)) { + float dist = p.x + 2.0 * p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + + if (dist > 1.0 + pixel_size / 2) { + return w4; + } + + vec4 r; + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist < 1.0 - pixel_size / 2) { + return r; + } + + return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); + } + + if (P(0x7e,0x2a) || P(0xef,0xab)) { + float dist = p.y + 2.0 * p.x; + float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); + + if (p.y + 2.0 * p.x > 1.0 + pixel_size / 2) { + return w4; + } + + vec4 r; + + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist < 1.0 - pixel_size / 2) { + return r; + } + + return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); + } + + if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) { + return mix(w4, w3, 0.5 - p.x); + } + + if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) { + return mix(w4, w1, 0.5 - p.y); + } + + if (P(0xfb,0x6a) || P(0x6f,0x6e) || P(0x3f,0x3e) || P(0xfb,0xfa) || + P(0xdf,0xde) || P(0xdf,0x1e)) { + return mix(w4, w0, (1.0 - p.x - p.y) / 2.0); + } + + if (P(0x4f,0x4b) || P(0x9f,0x1b) || P(0x2f,0x0b) || + P(0xbe,0x0a) || P(0xee,0x0a) || P(0x7e,0x0a) || P(0xeb,0x4b) || + P(0x3b,0x1b)) { + float dist = p.x + p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)); + + if (dist > 0.5 + pixel_size / 2) { + return w4; + } + + vec4 r; + if (is_different(w0, w1) || is_different(w0, w3)) { + r = mix(w1, w3, p.y - p.x + 0.5); + } + else { + r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); + } + + if (dist < 0.5 - pixel_size / 2) { + return r; + } + + return mix(r, w4, (dist + pixel_size / 2 - 0.5) / pixel_size); + } + + if (P(0x0b,0x01)) { + return mix(mix(w4, w3, 0.5 - p.x), mix(w1, (w1 + w3) / 2.0, 0.5 - p.x), 0.5 - p.y); + } + + if (P(0x0b,0x00)) { + return mix(mix(w4, w3, 0.5 - p.x), mix(w1, w0, 0.5 - p.x), 0.5 - p.y); + } + + float dist = p.x + p.y; + float pixel_size = length(1.0 / (output_resolution / input_resolution)); + + if (dist > 0.5 + pixel_size / 2) { + return w4; + } + + /* We need more samples to "solve" this diagonal */ + vec4 x0 = texture(image, position + vec2( -o.x * 2.0, -o.y * 2.0)); + vec4 x1 = texture(image, position + vec2( -o.x , -o.y * 2.0)); + vec4 x2 = texture(image, position + vec2( 0.0 , -o.y * 2.0)); + vec4 x3 = texture(image, position + vec2( o.x , -o.y * 2.0)); + vec4 x4 = texture(image, position + vec2( -o.x * 2.0, -o.y )); + vec4 x5 = texture(image, position + vec2( -o.x * 2.0, 0.0 )); + vec4 x6 = texture(image, position + vec2( -o.x * 2.0, o.y )); + + if (is_different(x0, w4)) pattern |= 1 << 8; + if (is_different(x1, w4)) pattern |= 1 << 9; + if (is_different(x2, w4)) pattern |= 1 << 10; + if (is_different(x3, w4)) pattern |= 1 << 11; + if (is_different(x4, w4)) pattern |= 1 << 12; + if (is_different(x5, w4)) pattern |= 1 << 13; + if (is_different(x6, w4)) pattern |= 1 << 14; + + int diagonal_bias = -7; + while (pattern != 0) { + diagonal_bias += pattern & 1; + pattern >>= 1; + } + + if (diagonal_bias <= 0) { + vec4 r = mix(w1, w3, p.y - p.x + 0.5); + if (dist < 0.5 - pixel_size / 2) { + return r; + } + return mix(r, w4, (dist + pixel_size / 2 - 0.5) / pixel_size); + } + + return w4; +} diff --git a/bsnes/gb/Shaders/OmniScaleLegacy.fsh b/bsnes/gb/Shaders/OmniScaleLegacy.fsh new file mode 100644 index 00000000..06849fdd --- /dev/null +++ b/bsnes/gb/Shaders/OmniScaleLegacy.fsh @@ -0,0 +1,104 @@ +STATIC float quickDistance(vec4 a, vec4 b) +{ + return abs(a.x - b.x) + abs(a.y - b.y) + abs(a.z - b.z); +} + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pixel = position * input_resolution - vec2(0.5, 0.5); + + vec4 q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + vec4 q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + vec4 q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + vec4 q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + vec2 pos = fract(pixel); + + /* Special handling for diaonals */ + bool hasDownDiagonal = false; + bool hasUpDiagonal = false; + if (equal(q12, q21) && inequal(q11, q22)) hasUpDiagonal = true; + else if (inequal(q12, q21) && equal(q11, q22)) hasDownDiagonal = true; + else if (equal(q12, q21) && equal(q11, q22)) { + if (equal(q11, q12)) return q11; + int diagonalBias = 0; + for (float y = -1.0; y < 3.0; y++) { + for (float x = -1.0; x < 3.0; x++) { + vec4 color = texture(image, (pixel + vec2(x, y)) / input_resolution); + if (equal(color, q11)) diagonalBias++; + if (equal(color, q12)) diagonalBias--; + } + } + if (diagonalBias <= 0) { + hasDownDiagonal = true; + } + if (diagonalBias >= 0) { + hasUpDiagonal = true; + } + } + + if (hasUpDiagonal || hasDownDiagonal) { + vec4 downDiagonalResult, upDiagonalResult; + + if (hasUpDiagonal) { + float diagonalPos = pos.x + pos.y; + + if (diagonalPos < 0.5) { + upDiagonalResult = q11; + } + else if (diagonalPos > 1.5) { + upDiagonalResult = q22; + } + else { + upDiagonalResult = q12; + } + } + + if (hasDownDiagonal) { + float diagonalPos = 1.0 - pos.x + pos.y; + + if (diagonalPos < 0.5) { + downDiagonalResult = q21; + } + else if (diagonalPos > 1.5) { + downDiagonalResult = q12; + } + else { + downDiagonalResult = q11; + } + } + + if (!hasUpDiagonal) return downDiagonalResult; + if (!hasDownDiagonal) return upDiagonalResult; + return mix(downDiagonalResult, upDiagonalResult, 0.5); + } + + vec4 r1 = mix(q11, q21, fract(pos.x)); + vec4 r2 = mix(q12, q22, fract(pos.x)); + + vec4 unqunatized = mix(r1, r2, fract(pos.y)); + + float q11d = quickDistance(unqunatized, q11); + float q21d = quickDistance(unqunatized, q21); + float q12d = quickDistance(unqunatized, q12); + float q22d = quickDistance(unqunatized, q22); + + float best = min(q11d, + min(q21d, + min(q12d, + q22d))); + + if (equal(q11d, best)) { + return q11; + } + + if (equal(q21d, best)) { + return q21; + } + + if (equal(q12d, best)) { + return q12; + } + + return q22; +} diff --git a/bsnes/gb/Shaders/Scale2x.fsh b/bsnes/gb/Shaders/Scale2x.fsh new file mode 100644 index 00000000..17b6edb8 --- /dev/null +++ b/bsnes/gb/Shaders/Scale2x.fsh @@ -0,0 +1,41 @@ +/* Shader implementation of Scale2x is adapted from https://gist.github.com/singron/3161079 */ + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / input_resolution; + + // texel arrangement + // A B C + // D E F + // G H I + // vec4 A = texture(image, position + vec2( -o.x, o.y)); + vec4 B = texture(image, position + vec2( 0, o.y)); + // vec4 C = texture(image, position + vec2( o.x, o.y)); + vec4 D = texture(image, position + vec2( -o.x, 0)); + vec4 E = texture(image, position + vec2( 0, 0)); + vec4 F = texture(image, position + vec2( o.x, 0)); + // vec4 G = texture(image, position + vec2( -o.x, -o.y)); + vec4 H = texture(image, position + vec2( 0, -o.y)); + // vec4 I = texture(image, position + vec2( o.x, -o.y)); + vec2 p = position * input_resolution; + // p = the position within a pixel [0...1] + p = fract(p); + if (p.x > .5) { + if (p.y > .5) { + // Top Right + return equal(B, F) && inequal(B, D) && inequal(F, H) ? F : E; + } else { + // Bottom Right + return equal(H, F) && inequal(D, H) && inequal(B, F) ? F : E; + } + } else { + if (p.y > .5) { + // Top Left + return equal(D, B) && inequal(B, F) && inequal(D, H) ? D : E; + } else { + // Bottom Left + return equal(D, H) && inequal(D, B) && inequal(H, F) ? D : E; + } + } +} diff --git a/bsnes/gb/Shaders/Scale4x.fsh b/bsnes/gb/Shaders/Scale4x.fsh new file mode 100644 index 00000000..da1ff148 --- /dev/null +++ b/bsnes/gb/Shaders/Scale4x.fsh @@ -0,0 +1,79 @@ +STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / input_resolution; + // texel arrangement + // A B C + // D E F + // G H I + // vec4 A = texture(image, position + vec2( -o.x, o.y)); + vec4 B = texture(image, position + vec2( 0, o.y)); + // vec4 C = texture(image, position + vec2( o.x, o.y)); + vec4 D = texture(image, position + vec2( -o.x, 0)); + vec4 E = texture(image, position + vec2( 0, 0)); + vec4 F = texture(image, position + vec2( o.x, 0)); + // vec4 G = texture(image, position + vec2( -o.x, -o.y)); + vec4 H = texture(image, position + vec2( 0, -o.y)); + // vec4 I = texture(image, position + vec2( o.x, -o.y)); + vec2 p = position * input_resolution; + // p = the position within a pixel [0...1] + vec4 R; + p = fract(p); + if (p.x > .5) { + if (p.y > .5) { + // Top Right + return equal(B, F) && inequal(B, D) && inequal(F, H) ? F : E; + } else { + // Bottom Right + return equal(H, F) && inequal(D, H) && inequal(B, F) ? F : E; + } + } else { + if (p.y > .5) { + // Top Left + return equal(D, B) && inequal(B, F) && inequal(D, H) ? D : E; + } else { + // Bottom Left + return equal(D, H) && inequal(D, B) && inequal(H, F) ? D : E; + } + } +} + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + // o = offset, the width of a pixel + vec2 o = 1.0 / (input_resolution * 2.); + + // texel arrangement + // A B C + // D E F + // G H I + // vec4 A = scale2x(image, position + vec2( -o.x, o.y), input_resolution, output_resolution); + vec4 B = scale2x(image, position + vec2( 0, o.y), input_resolution, output_resolution); + // vec4 C = scale2x(image, position + vec2( o.x, o.y), input_resolution, output_resolution); + vec4 D = scale2x(image, position + vec2( -o.x, 0), input_resolution, output_resolution); + vec4 E = scale2x(image, position + vec2( 0, 0), input_resolution, output_resolution); + vec4 F = scale2x(image, position + vec2( o.x, 0), input_resolution, output_resolution); + // vec4 G = scale2x(image, position + vec2( -o.x, -o.y), input_resolution, output_resolution); + vec4 H = scale2x(image, position + vec2( 0, -o.y), input_resolution, output_resolution); + // vec4 I = scale2x(image, position + vec2( o.x, -o.y), input_resolution, output_resolution); + vec2 p = position * input_resolution * 2.; + // p = the position within a pixel [0...1] + p = fract(p); + if (p.x > .5) { + if (p.y > .5) { + // Top Right + return equal(B, F) && inequal(B, D) && inequal(F, H) ? F : E; + } else { + // Bottom Right + return equal(H, F) && inequal(D, H) && inequal(B, F) ? F : E; + } + } else { + if (p.y > .5) { + // Top Left + return equal(D, B) && inequal(B, F) && inequal(D, H) ? D : E; + } else { + // Bottom Left + return equal(D, H) && inequal(D, B) && inequal(H, F) ? D : E; + } + } +} diff --git a/bsnes/gb/Shaders/SmoothBilinear.fsh b/bsnes/gb/Shaders/SmoothBilinear.fsh new file mode 100644 index 00000000..d5082772 --- /dev/null +++ b/bsnes/gb/Shaders/SmoothBilinear.fsh @@ -0,0 +1,16 @@ +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + vec2 pixel = position * input_resolution - vec2(0.5, 0.5); + + vec4 q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); + vec4 q12 = texture(image, (vec2(floor(pixel.x), ceil(pixel.y)) + 0.5) / input_resolution); + vec4 q21 = texture(image, (vec2(ceil(pixel.x), floor(pixel.y)) + 0.5) / input_resolution); + vec4 q22 = texture(image, (ceil(pixel) + 0.5) / input_resolution); + + vec2 s = smoothstep(0., 1., fract(pixel)); + + vec4 r1 = mix(q11, q21, s.x); + vec4 r2 = mix(q12, q22, s.x); + + return mix (r1, r2, s.y); +} diff --git a/bsnes/gb/Tester/main.c b/bsnes/gb/Tester/main.c new file mode 100755 index 00000000..16dbf7bb --- /dev/null +++ b/bsnes/gb/Tester/main.c @@ -0,0 +1,440 @@ +// The tester requires low-level access to the GB struct to detect failures +#define GB_INTERNAL + +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#define snprintf _snprintf +#else +#include +#endif + +#include +#include + +static bool running = false; +static char *filename; +static char *bmp_filename; +static char *log_filename; +static FILE *log_file; +static void replace_extension(const char *src, size_t length, char *dest, const char *ext); +static bool push_start_a, start_is_not_first, a_is_bad, b_is_confirm, push_faster, push_slower, + do_not_stop, push_a_twice, start_is_bad, allow_weird_sp_values, large_stack, push_right, + semi_random, limit_start, pointer_control; +static unsigned int test_length = 60 * 40; +GB_gameboy_t gb; + +static unsigned int frames = 0; +const char bmp_header[] = { +0x42, 0x4D, 0x48, 0x68, 0x01, 0x00, 0x00, 0x00, +0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x38, 0x00, +0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x70, 0xFF, +0xFF, 0xFF, 0x01, 0x00, 0x20, 0x00, 0x03, 0x00, +0x00, 0x00, 0x02, 0x68, 0x01, 0x00, 0x12, 0x0B, +0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +uint32_t bitmap[160*144]; + +static char *async_input_callback(GB_gameboy_t *gb) +{ + return NULL; +} + +static void handle_buttons(GB_gameboy_t *gb) +{ + /* Do not press any buttons during the last two seconds, this might cause a + screenshot to be taken while the LCD is off if the press makes the game + load graphics. */ + if (push_start_a && (frames < test_length - 120 || do_not_stop)) { + unsigned combo_length = 40; + if (start_is_not_first || push_a_twice) combo_length = 60; /* The start item in the menu is not the first, so also push down */ + else if (a_is_bad || start_is_bad) combo_length = 20; /* Pressing A has a negative effect (when trying to start the game). */ + + if (semi_random) { + if (frames % 10 == 0) { + unsigned key = (((frames / 20) * 0x1337cafe) >> 29) & 7; + gb->keys[0][key] = (frames % 20) == 0; + } + } + else { + switch ((push_faster ? frames * 2 : + push_slower ? frames / 2 : + push_a_twice? frames / 4: + frames) % combo_length + (start_is_bad? 20 : 0) ) { + case 0: + if (!limit_start || frames < 20 * 60) { + GB_set_key_state(gb, push_right? GB_KEY_RIGHT: GB_KEY_START, true); + } + if (pointer_control) { + GB_set_key_state(gb, GB_KEY_LEFT, true); + GB_set_key_state(gb, GB_KEY_UP, true); + } + + break; + case 10: + GB_set_key_state(gb, push_right? GB_KEY_RIGHT: GB_KEY_START, false); + if (pointer_control) { + GB_set_key_state(gb, GB_KEY_LEFT, false); + GB_set_key_state(gb, GB_KEY_UP, false); + } + break; + case 20: + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, true); + break; + case 30: + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, false); + break; + case 40: + if (push_a_twice) { + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, true); + } + else if (gb->boot_rom_finished) { + GB_set_key_state(gb, GB_KEY_DOWN, true); + } + break; + case 50: + GB_set_key_state(gb, b_is_confirm? GB_KEY_B: GB_KEY_A, false); + GB_set_key_state(gb, GB_KEY_DOWN, false); + break; + } + } + } + +} + +static void vblank(GB_gameboy_t *gb) +{ + /* Detect common crashes and stop the test early */ + if (frames < test_length - 1) { + if (gb->backtrace_size >= 0x200 + (large_stack? 0x80: 0) || (!allow_weird_sp_values && (gb->registers[GB_REGISTER_SP] >= 0xfe00 && gb->registers[GB_REGISTER_SP] < 0xff80))) { + GB_log(gb, "A stack overflow has probably occurred. (SP = $%04x; backtrace size = %d) \n", + gb->registers[GB_REGISTER_SP], gb->backtrace_size); + frames = test_length - 1; + } + if (gb->halted && !gb->interrupt_enable) { + GB_log(gb, "The game is deadlocked.\n"); + frames = test_length - 1; + } + } + + if (frames >= test_length && !gb->disable_rendering) { + bool is_screen_blank = true; + for (unsigned i = 160*144; i--;) { + if (bitmap[i] != bitmap[0]) { + is_screen_blank = false; + break; + } + } + + /* Let the test run for extra four seconds if the screen is off/disabled */ + if (!is_screen_blank || frames >= test_length + 60 * 4) { + FILE *f = fopen(bmp_filename, "wb"); + fwrite(&bmp_header, 1, sizeof(bmp_header), f); + fwrite(&bitmap, 1, sizeof(bitmap), f); + fclose(f); + if (!gb->boot_rom_finished) { + GB_log(gb, "Boot ROM did not finish.\n"); + } + if (is_screen_blank) { + GB_log(gb, "Game probably stuck with blank screen. \n"); + } + running = false; + } + } + else if (frames >= test_length - 1) { + gb->disable_rendering = false; + } +} + +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + if (!log_file) log_file = fopen(log_filename, "w"); + fprintf(log_file, "%s", string); +} + +#ifdef __APPLE__ +#include +#endif + +static const char *executable_folder(void) +{ + static char path[1024] = {0,}; + if (path[0]) { + return path; + } + /* Ugly unportable code! :( */ +#ifdef __APPLE__ + uint32_t length = sizeof(path) - 1; + _NSGetExecutablePath(&path[0], &length); +#else +#ifdef __linux__ + size_t __attribute__((unused)) length = readlink("/proc/self/exe", &path[0], sizeof(path) - 1); + assert(length != -1); +#else +#ifdef _WIN32 + HMODULE hModule = GetModuleHandle(NULL); + GetModuleFileName(hModule, path, sizeof(path) - 1); +#else + /* No OS-specific way, assume running from CWD */ + getcwd(&path[0], sizeof(path) - 1); + return path; +#endif +#endif +#endif + size_t pos = strlen(path); + while (pos) { + pos--; +#ifdef _WIN32 + if (path[pos] == '\\') { +#else + if (path[pos] == '/') { +#endif + path[pos] = 0; + break; + } + } + return path; +} + +static char *executable_relative_path(const char *filename) +{ + static char path[1024]; + snprintf(path, sizeof(path), "%s/%s", executable_folder(), filename); + return path; +} + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return (r << 24) | (g << 16) | (b << 8); +} + +static void replace_extension(const char *src, size_t length, char *dest, const char *ext) +{ + memcpy(dest, src, length); + dest[length] = 0; + + /* Remove extension */ + for (size_t i = length; i--;) { + if (dest[i] == '/') break; + if (dest[i] == '.') { + dest[i] = 0; + break; + } + } + + /* Add new extension */ + strcat(dest, ext); +} + + +int main(int argc, char **argv) +{ +#define str(x) #x +#define xstr(x) str(x) + fprintf(stderr, "SameBoy Tester v" xstr(VERSION) "\n"); + + if (argc == 1) { + fprintf(stderr, "Usage: %s [--dmg] [--start] [--length seconds] [--boot path to boot ROM]" +#ifndef _WIN32 + " [--jobs number of tests to run simultaneously]" +#endif + " rom ...\n", argv[0]); + exit(1); + } + +#ifndef _WIN32 + unsigned int max_forks = 1; + unsigned int current_forks = 0; +#endif + + bool dmg = false; + const char *boot_rom_path = NULL; + + GB_random_set_enabled(false); + + for (unsigned i = 1; i < argc; i++) { + if (strcmp(argv[i], "--dmg") == 0) { + fprintf(stderr, "Using DMG mode\n"); + dmg = true; + continue; + } + + if (strcmp(argv[i], "--start") == 0) { + fprintf(stderr, "Pushing Start and A\n"); + push_start_a = true; + continue; + } + + if (strcmp(argv[i], "--length") == 0 && i != argc - 1) { + test_length = atoi(argv[++i]) * 60; + fprintf(stderr, "Test length is %d seconds\n", test_length / 60); + continue; + } + + if (strcmp(argv[i], "--boot") == 0 && i != argc - 1) { + fprintf(stderr, "Using boot ROM %s\n", argv[i + 1]); + boot_rom_path = argv[++i]; + continue; + } + +#ifndef _WIN32 + if (strcmp(argv[i], "--jobs") == 0 && i != argc - 1) { + max_forks = atoi(argv[++i]); + /* Make sure wrong input doesn't blow anything up. */ + if (max_forks < 1) max_forks = 1; + if (max_forks > 16) max_forks = 16; + fprintf(stderr, "Running up to %d tests simultaneously\n", max_forks); + continue; + } + + if (max_forks > 1) { + while (current_forks >= max_forks) { + int wait_out; + while (wait(&wait_out) == -1); + current_forks--; + } + + current_forks++; + if (fork() != 0) continue; + } +#endif + filename = argv[i]; + size_t path_length = strlen(filename); + + char bitmap_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .bmp + NULL */ + replace_extension(filename, path_length, bitmap_path, ".bmp"); + bmp_filename = &bitmap_path[0]; + + char log_path[path_length + 5]; + replace_extension(filename, path_length, log_path, ".log"); + log_filename = &log_path[0]; + + fprintf(stderr, "Testing ROM %s\n", filename); + + if (dmg) { + GB_init(&gb, GB_MODEL_DMG_B); + if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("dmg_boot.bin"))) { + fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("dmg_boot.bin")); + exit(1); + } + } + else { + GB_init(&gb, GB_MODEL_CGB_E); + if (GB_load_boot_rom(&gb, boot_rom_path ?: executable_relative_path("cgb_boot.bin"))) { + fprintf(stderr, "Failed to load boot ROM from '%s'\n", boot_rom_path ?: executable_relative_path("cgb_boot.bin")); + exit(1); + } + } + + GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); + GB_set_pixels_output(&gb, &bitmap[0]); + GB_set_rgb_encode_callback(&gb, rgb_encode); + GB_set_log_callback(&gb, log_callback); + GB_set_async_input_callback(&gb, async_input_callback); + GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_EMULATE_HARDWARE); + + if (GB_load_rom(&gb, filename)) { + perror("Failed to load ROM"); + exit(1); + } + + /* Game specific hacks for start attempt automations */ + /* It's OK. No overflow is possible here. */ + start_is_not_first = strcmp((const char *)(gb.rom + 0x134), "NEKOJARA") == 0 || + strcmp((const char *)(gb.rom + 0x134), "GINGA") == 0; + a_is_bad = strcmp((const char *)(gb.rom + 0x134), "DESERT STRIKE") == 0 || + /* Restarting in Puzzle Boy/Kwirk (Start followed by A) leaks stack. */ + strcmp((const char *)(gb.rom + 0x134), "KWIRK") == 0 || + strcmp((const char *)(gb.rom + 0x134), "PUZZLE BOY") == 0; + start_is_bad = strcmp((const char *)(gb.rom + 0x134), "BLUESALPHA") == 0 || + strcmp((const char *)(gb.rom + 0x134), "ONI 5") == 0; + b_is_confirm = strcmp((const char *)(gb.rom + 0x134), "ELITE SOCCER") == 0 || + strcmp((const char *)(gb.rom + 0x134), "SOCCER") == 0 || + strcmp((const char *)(gb.rom + 0x134), "GEX GECKO") == 0; + push_faster = strcmp((const char *)(gb.rom + 0x134), "MOGURA DE PON!") == 0 || + strcmp((const char *)(gb.rom + 0x134), "HUGO2 1/2") == 0 || + strcmp((const char *)(gb.rom + 0x134), "HUGO") == 0; + push_slower = strcmp((const char *)(gb.rom + 0x134), "BAKENOU") == 0; + do_not_stop = strcmp((const char *)(gb.rom + 0x134), "SPACE INVADERS") == 0; + push_right = memcmp((const char *)(gb.rom + 0x134), "BOB ET BOB", strlen("BOB ET BOB")) == 0 || + strcmp((const char *)(gb.rom + 0x134), "LITTLE MASTER") == 0 || + /* M&M's Minis Madness Demo (which has no menu but the same title as the full game) */ + (memcmp((const char *)(gb.rom + 0x134), "MINIMADNESSBMIE", strlen("MINIMADNESSBMIE")) == 0 && + gb.rom[0x14e] == 0x6c); + /* This game has some terrible menus. */ + semi_random = strcmp((const char *)(gb.rom + 0x134), "KUKU GAME") == 0; + + + + /* This game temporarily sets SP to OAM RAM */ + allow_weird_sp_values = strcmp((const char *)(gb.rom + 0x134), "WDL:TT") == 0 || + /* Some mooneye-gb tests abuse the stack */ + strcmp((const char *)(gb.rom + 0x134), "mooneye-gb test") == 0; + + /* This game uses some recursive algorithms and therefore requires quite a large call stack */ + large_stack = memcmp((const char *)(gb.rom + 0x134), "MICRO EPAK1BM", strlen("MICRO EPAK1BM")) == 0 || + strcmp((const char *)(gb.rom + 0x134), "TECMO BOWL") == 0; + /* High quality game that leaks stack whenever you open the menu (with start), + but requires pressing start to play it. */ + limit_start = strcmp((const char *)(gb.rom + 0x134), "DIVA STARS") == 0; + large_stack |= limit_start; + + /* Pressing start while in the map in Tsuri Sensei will leak an internal screen-stack which + will eventually overflow, override an array of jump-table indexes, jump to a random + address, execute an invalid opcode, and crash. Pressing A twice while slowing down + will prevent this scenario. */ + push_a_twice = strcmp((const char *)(gb.rom + 0x134), "TURI SENSEI V1") == 0; + + /* Yes, you should totally use a cursor point & click interface for the language select menu. */ + pointer_control = memcmp((const char *)(gb.rom + 0x134), "LEGO ATEAM BLPP", strlen("LEGO ATEAM BLPP")) == 0; + push_faster |= pointer_control; + + /* Run emulation */ + running = true; + gb.turbo = gb.turbo_dont_skip = gb.disable_rendering = true; + frames = 0; + unsigned cycles = 0; + while (running) { + cycles += GB_run(&gb); + if (cycles >= 139810) { /* Approximately 1/60 a second. Intentionally not the actual length of a frame. */ + handle_buttons(&gb); + cycles -= 139810; + frames++; + } + /* This early crash test must not run in vblank because PC might not point to the next instruction. */ + if (gb.pc == 0x38 && frames < test_length - 1 && GB_read_memory(&gb, 0x38) == 0xFF) { + GB_log(&gb, "The game is probably stuck in an FF loop.\n"); + frames = test_length - 1; + } + } + + + if (log_file) { + fclose(log_file); + log_file = NULL; + } + + GB_free(&gb); +#ifndef _WIN32 + if (max_forks > 1) { + exit(0); + } +#endif + } +#ifndef _WIN32 + int wait_out; + while (wait(&wait_out) != -1); +#endif + return 0; +} + diff --git a/bsnes/gb/Windows/inttypes.h b/bsnes/gb/Windows/inttypes.h new file mode 100755 index 00000000..9a6118bd --- /dev/null +++ b/bsnes/gb/Windows/inttypes.h @@ -0,0 +1 @@ +#include diff --git a/bsnes/gb/Windows/math.h b/bsnes/gb/Windows/math.h new file mode 100755 index 00000000..2b934f90 --- /dev/null +++ b/bsnes/gb/Windows/math.h @@ -0,0 +1,9 @@ +#pragma once +#include_next +#ifndef __MINGW32__ +/* "Old" (Pre-2015) Windows headers/libc don't have round. */ +static inline double round(double f) +{ + return f >= 0? (int)(f + 0.5) : (int)(f - 0.5); +} +#endif \ No newline at end of file diff --git a/bsnes/gb/Windows/resources.rc b/bsnes/gb/Windows/resources.rc new file mode 100644 index 00000000..73c12139 Binary files /dev/null and b/bsnes/gb/Windows/resources.rc differ diff --git a/bsnes/gb/Windows/sameboy.ico b/bsnes/gb/Windows/sameboy.ico new file mode 100644 index 00000000..685523e5 Binary files /dev/null and b/bsnes/gb/Windows/sameboy.ico differ diff --git a/bsnes/gb/Windows/stdint.h b/bsnes/gb/Windows/stdint.h new file mode 100755 index 00000000..cbe84d56 --- /dev/null +++ b/bsnes/gb/Windows/stdint.h @@ -0,0 +1,3 @@ +#pragma once +#include_next +typedef intptr_t ssize_t; \ No newline at end of file diff --git a/bsnes/gb/Windows/stdio.h b/bsnes/gb/Windows/stdio.h new file mode 100755 index 00000000..ef21ea48 --- /dev/null +++ b/bsnes/gb/Windows/stdio.h @@ -0,0 +1,80 @@ +#pragma once +#include_next +#include + +int access(const char *filename, int mode); +#define R_OK 2 +#define W_OK 4 + +#ifndef __MINGW32__ +#ifndef __LIBRETRO__ +static inline int vasprintf(char **str, const char *fmt, va_list args) +{ + size_t size = _vscprintf(fmt, args) + 1; + *str = malloc(size); + int ret = vsprintf(*str, fmt, args); + if (ret != size - 1) { + free(*str); + *str = NULL; + return -1; + } + return ret; +} +#endif +#endif + +/* This code is public domain -- Will Hartung 4/9/09 */ +static inline size_t getline(char **lineptr, size_t *n, FILE *stream) +{ + char *bufptr = NULL; + char *p = bufptr; + size_t size; + int c; + + if (lineptr == NULL) { + return -1; + } + if (stream == NULL) { + return -1; + } + if (n == NULL) { + return -1; + } + bufptr = *lineptr; + size = *n; + + c = fgetc(stream); + if (c == EOF) { + return -1; + } + if (bufptr == NULL) { + bufptr = malloc(128); + if (bufptr == NULL) { + return -1; + } + size = 128; + } + p = bufptr; + while (c != EOF) { + if ((p - bufptr) > (size - 1)) { + size = size + 128; + bufptr = realloc(bufptr, size); + if (bufptr == NULL) { + return -1; + } + } + *p++ = c; + if (c == '\n') { + break; + } + c = fgetc(stream); + } + + *p++ = '\0'; + *lineptr = bufptr; + *n = size; + + return p - bufptr - 1; +} + +#define snprintf _snprintf diff --git a/bsnes/gb/Windows/string.h b/bsnes/gb/Windows/string.h new file mode 100755 index 00000000..b899ca97 --- /dev/null +++ b/bsnes/gb/Windows/string.h @@ -0,0 +1,3 @@ +#pragma once +#include_next +#define strdup _strdup \ No newline at end of file diff --git a/bsnes/gb/Windows/unistd.h b/bsnes/gb/Windows/unistd.h new file mode 100644 index 00000000..b7aabf29 --- /dev/null +++ b/bsnes/gb/Windows/unistd.h @@ -0,0 +1,7 @@ +#include +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 + +#define read(...) _read(__VA_ARGS__) +#define write(...) _write(__VA_ARGS__) diff --git a/bsnes/gb/Windows/utf8_compat.c b/bsnes/gb/Windows/utf8_compat.c new file mode 100755 index 00000000..03472115 --- /dev/null +++ b/bsnes/gb/Windows/utf8_compat.c @@ -0,0 +1,24 @@ +#include +#include +#include +#include + +FILE *fopen(const char *filename, const char *mode) +{ + wchar_t w_filename[MAX_PATH] = {0,}; + MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, sizeof(w_filename) / sizeof(w_filename[0])); + + wchar_t w_mode[8] = {0,}; + MultiByteToWideChar(CP_UTF8, 0, mode, -1, w_mode, sizeof(w_mode) / sizeof(w_mode[0])); + + return _wfopen(w_filename, w_mode); +} + +int access(const char *filename, int mode) +{ + wchar_t w_filename[MAX_PATH] = {0,}; + MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, sizeof(w_filename) / sizeof(w_filename[0])); + + return _waccess(w_filename, mode); +} + diff --git a/bsnes/gb/build-faq.md b/bsnes/gb/build-faq.md new file mode 100644 index 00000000..9def1349 --- /dev/null +++ b/bsnes/gb/build-faq.md @@ -0,0 +1,57 @@ +# macOS Specific Issues +## Attempting to build the Cocoa frontend fails with NSInternalInconsistencyException + +When building on macOS, the build system will make a native Cocoa app by default. In this case, the build system uses the Xcode `ibtool` command to build user interface files. If this command fails, you can fix this issue by starting Xcode and letting it install components. After this is done, you should be able to close Xcode and build successfully. + +## Attempting to build the SDL frontend on macOS fails on linking + +SameBoy on macOS expects you to have SDL2 installed via Brew, and not as a framework. Older versions expected it to be installed as a framework, but this is no longer the case. + +# Windows Build Process + +## Tools and Libraries Installation + +For the various tools and libraries, follow the below guide to ensure easy, proper configuration for the build environment: + +### SDL2 + +For [libSDL2](https://libsdl.org/download-2.0.php), download the Visual C++ Development Library pack. Place the extracted files within a known folder for later. Both the `\x86\` and `\include\` paths will be needed. + +The following examples will be referenced later: + +- `C:\SDL2\lib\x86\*` +- `C:\SDL2\include\*` + +### rgbds + +After downloading [rgbds](https://github.com/bentley/rgbds/releases/), ensure that it is added to the `%PATH%`. This may be done by adding it to the user's or SYSTEM's Environment Variables, or may be added to the command line at compilation time via `set path=%path%;C:\path\to\rgbds`. + +### GnuWin + +Ensure that the `gnuwin32\bin\` directory is included in `%PATH%`. Like rgbds above, this may instead be manually included on the command line before installation: `set path=%path%;C:\path\to\gnuwin32\bin`. + +## Building + +Within a command prompt in the project directory: + +``` +vcvars32 +set lib=%lib%;C:\SDL2\lib\x86 +set include=%include%;C:\SDL2\include +make +``` +Please note that these directories (`C:\SDL2\*`) are the examples given within the "SDL Port" section above. Ensure that your `%PATH%` properly includes `rgbds` and `gnuwin32\bin`, and that the `lib` and `include` paths include the appropriate SDL2 directories. + +## Common Errors + +### Error -1073741819 + +If encountering an error that appears as follows: + +``` make: *** [build/bin/BootROMs/dmg_boot.bin] Error -1073741819``` + +Simply run `make` again, and the process will continue. This appears to happen occasionally with `build/bin/BootROMs/dmg_boot.bin` and `build/bin/BootROMs/sgb2_boot.bin`. It does not affect the compiled output. This appears to be an issue with GnuWin. + +### The system cannot find the file specified (`usr/bin/mkdir`) + +If errors arise (i.e., particularly with the `CREATE_PROCESS('usr/bin/mkdir')` calls, also verify that Git for Windows has not been installed with full Linux support. If it has, remove `C:\Program Files\Git\usr\bin` from the SYSTEM %PATH% until after compilation. This happens because the Git for Windows version of `which` is used instead of the GnuWin one, and it returns a Unix-style path instead of a Windows one. diff --git a/bsnes/gb/libretro/Makefile b/bsnes/gb/libretro/Makefile new file mode 100644 index 00000000..2ed87b8c --- /dev/null +++ b/bsnes/gb/libretro/Makefile @@ -0,0 +1,362 @@ +STATIC_LINKING := 0 +AR := ar + +CFLAGS := -Wall $(CFLAGS) + +GIT_VERSION ?= " $(shell git rev-parse --short HEAD || echo unknown)" +ifneq ($(GIT_VERSION)," unknown") + CFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\" +endif + +SPACE := +SPACE := $(SPACE) $(SPACE) +BACKSLASH := +BACKSLASH := \$(BACKSLASH) +filter_out1 = $(filter-out $(firstword $1),$1) +filter_out2 = $(call filter_out1,$(call filter_out1,$1)) +unixpath = $(subst \,/,$1) +unixcygpath = /$(subst :,,$(call unixpath,$1)) + +ifeq ($(platform),) +platform = unix +ifeq ($(shell uname -a),) + platform = win +else ifneq ($(findstring MINGW,$(shell uname -a)),) + platform = win +else ifneq ($(findstring Darwin,$(shell uname -a)),) + platform = osx +else ifneq ($(findstring win,$(shell uname -a)),) + platform = win +endif +endif + +# system platform +system_platform = unix +ifeq ($(shell uname -a),) + EXE_EXT = .exe + system_platform = win +else ifneq ($(findstring Darwin,$(shell uname -a)),) + system_platform = osx + arch = intel +ifeq ($(shell uname -p),powerpc) + arch = ppc +endif +else ifneq ($(findstring MINGW,$(shell uname -a)),) + system_platform = win +endif + +ifeq ($(platform), win) + INCFLAGS += -I Windows +endif + +CORE_DIR = ../ + +TARGET_NAME = sameboy +LIBM = -lm + +ifeq ($(ARCHFLAGS),) +ifeq ($(archs),ppc) + ARCHFLAGS = -arch ppc -arch ppc64 +else + ARCHFLAGS = -arch i386 -arch x86_64 +endif +endif + +ifneq ($(SANITIZER),) + CFLAGS := -fsanitize=$(SANITIZER) $(CFLAGS) + CXXFLAGS := -fsanitize=$(SANITIZER) $(CXXFLAGS) + LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS) -lasan +endif + +ifeq ($(platform), osx) +ifndef ($(NOUNIVERSAL)) + CFLAGS += $(ARCHFLAGS) + LFLAGS += $(ARCHFLAGS) +endif +endif + +ifeq ($(STATIC_LINKING), 1) +EXT := a +endif + +ifeq ($(platform), unix) + EXT ?= so + TARGET := $(TARGET_NAME)_libretro.$(EXT) + fpic := -fPIC + SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined +else ifeq ($(platform), linux-portable) + TARGET := $(TARGET_NAME)_libretro.$(EXT) + fpic := -fPIC -nostdlib + SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T + LIBM := +# (armv7 a7, hard point, neon based) ### +# NESC, SNESC, C64 mini +else ifeq ($(platform), classic_armv7_a7) + TARGET := $(TARGET_NAME)_libretro.so + fpic := -fPIC + SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined + CFLAGS += -Ofast \ + -flto=4 -fwhole-program -fuse-linker-plugin \ + -fdata-sections -ffunction-sections -Wl,--gc-sections \ + -fno-stack-protector -fno-ident -fomit-frame-pointer \ + -falign-functions=1 -falign-jumps=1 -falign-loops=1 \ + -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-unroll-loops \ + -fmerge-all-constants -fno-math-errno \ + -marm -mtune=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard + CXXFLAGS += $(CFLAGS) + CPPFLAGS += $(CFLAGS) + ASFLAGS += $(CFLAGS) + HAVE_NEON = 1 + ARCH = arm + BUILTIN_GPU = neon + USE_DYNAREC = 1 + ifeq ($(shell echo `$(CC) -dumpversion` "< 4.9" | bc -l), 1) + CFLAGS += -march=armv7-a + else + CFLAGS += -march=armv7ve + # If gcc is 5.0 or later + ifeq ($(shell echo `$(CC) -dumpversion` ">= 5" | bc -l), 1) + LDFLAGS += -static-libgcc -static-libstdc++ + endif + endif +####################################### +# Nintendo Switch (libtransistor) +else ifeq ($(platform), switch) + TARGET := $(TARGET_NAME)_libretro_$(platform).a + include $(LIBTRANSISTOR_HOME)/libtransistor.mk + CFLAGS += -Wl,-q -O3 -fno-short-enums -fno-optimize-sibling-calls + STATIC_LINKING=1 +# Nintendo Switch (libnx) +else ifeq ($(platform), libnx) + include $(DEVKITPRO)/libnx/switch_rules + TARGET := $(TARGET_NAME)_libretro_$(platform).a + DEFINES += -DSWITCH=1 -D__SWITCH__ -DARM + CFLAGS += $(DEFINES) -fPIE -I$(LIBNX)/include/ -ffunction-sections -fdata-sections -ftls-model=local-exec + CFLAGS += -march=armv8-a -mtune=cortex-a57 -mtp=soft -mcpu=cortex-a57+crc+fp+simd -ffast-math + CXXFLAGS := $(ASFLAGS) $(CFLAGS) + STATIC_LINKING = 1 +# Nintendo WiiU +else ifeq ($(platform), wiiu) + TARGET := $(TARGET_NAME)_libretro_$(platform).a + CC ?= $(DEVKITPPC)/bin/powerpc-eabi-gcc$(EXE_EXT) + AR ?= $(DEVKITPPC)/bin/powerpc-eabi-ar$(EXE_EXT) + CFLAGS += -DGEKKO -DHW_RVL -DWIIU -mwup -mcpu=750 -meabi -mhard-float -D__ppc__ -DMSB_FIRST -I$(DEVKITPRO)/libogc/include + CFLAGS += -U__INT32_TYPE__ -U __UINT32_TYPE__ -D__INT32_TYPE__=int + STATIC_LINKING = 1 +else ifneq (,$(findstring osx,$(platform))) + TARGET := $(TARGET_NAME)_libretro.dylib + fpic := -fPIC + SHARED := -dynamiclib +else ifneq (,$(findstring ios,$(platform))) + TARGET := $(TARGET_NAME)_libretro_ios.dylib + fpic := -fPIC + SHARED := -dynamiclib + +ifeq ($(IOSSDK),) + IOSSDK := $(shell xcodebuild -version -sdk iphoneos Path) +endif + + DEFINES := -DIOS +ifeq ($(platform),ios-arm64) + CC = cc -arch armv64 -isysroot $(IOSSDK) +else + CC = cc -arch armv7 -isysroot $(IOSSDK) +endif +ifeq ($(platform),$(filter $(platform),ios9 ios-arm64)) +CC += -miphoneos-version-min=8.0 +CFLAGS += -miphoneos-version-min=8.0 +else +CC += -miphoneos-version-min=5.0 +CFLAGS += -miphoneos-version-min=5.0 +endif +else ifneq (,$(findstring qnx,$(platform))) + TARGET := $(TARGET_NAME)_libretro_qnx.so + fpic := -fPIC + SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined +else ifeq ($(platform), emscripten) + TARGET := $(TARGET_NAME)_libretro_emscripten.bc + fpic := -fPIC + SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined +else ifeq ($(platform), vita) + TARGET := $(TARGET_NAME)_libretro_vita.a + CC = arm-vita-eabi-gcc + AR = arm-vita-eabi-ar + CFLAGS += -Wl,-q -O3 -fno-short-enums -fno-optimize-sibling-calls + STATIC_LINKING = 1 + +# Windows MSVC 2017 all architectures +else ifneq (,$(findstring windows_msvc2017,$(platform))) + + NO_GCC := 1 + CFLAGS += -DNOMINMAX + CXXFLAGS += -DNOMINMAX + WINDOWS_VERSION = 1 + + PlatformSuffix = $(subst windows_msvc2017_,,$(platform)) + ifneq (,$(findstring desktop,$(PlatformSuffix))) + WinPartition = desktop + MSVC2017CompileFlags = -DWINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP -FS + LDFLAGS += -MANIFEST -LTCG:incremental -NXCOMPAT -DYNAMICBASE -DEBUG -OPT:REF -INCREMENTAL:NO -SUBSYSTEM:WINDOWS -MANIFESTUAC:"level='asInvoker' uiAccess='false'" -OPT:ICF -ERRORREPORT:PROMPT -NOLOGO -TLBID:1 + LIBS += kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib + else ifneq (,$(findstring uwp,$(PlatformSuffix))) + WinPartition = uwp + MSVC2017CompileFlags = -DWINAPI_FAMILY=WINAPI_FAMILY_APP -D_WINDLL -D_UNICODE -DUNICODE -D__WRL_NO_DEFAULT_LIB__ -EHsc -FS + LDFLAGS += -APPCONTAINER -NXCOMPAT -DYNAMICBASE -MANIFEST:NO -LTCG -OPT:REF -SUBSYSTEM:CONSOLE -MANIFESTUAC:NO -OPT:ICF -ERRORREPORT:PROMPT -NOLOGO -TLBID:1 -DEBUG:FULL -WINMD:NO + LIBS += WindowsApp.lib + endif + + CFLAGS += $(MSVC2017CompileFlags) + CXXFLAGS += $(MSVC2017CompileFlags) + + TargetArchMoniker = $(subst $(WinPartition)_,,$(PlatformSuffix)) + + CC ?= cl.exe + CXX ?= cl.exe + LD ?= link.exe + + reg_query = $(call filter_out2,$(subst $2,,$(shell reg query "$2" -v "$1" 2>nul))) + fix_path = $(subst $(SPACE),\ ,$(subst \,/,$1)) + + ProgramFiles86w := $(shell cmd //c "echo %PROGRAMFILES(x86)%") + ProgramFiles86 := $(shell cygpath "$(ProgramFiles86w)") + + WindowsSdkDir ?= $(call reg_query,InstallationFolder,HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0) + WindowsSdkDir ?= $(call reg_query,InstallationFolder,HKEY_CURRENT_USER\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0) + WindowsSdkDir ?= $(call reg_query,InstallationFolder,HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0) + WindowsSdkDir ?= $(call reg_query,InstallationFolder,HKEY_CURRENT_USER\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0) + WindowsSdkDir := $(WindowsSdkDir) + + WindowsSDKVersion ?= $(firstword $(foreach folder,$(subst $(subst \,/,$(WindowsSdkDir)Include/),,$(wildcard $(call fix_path,$(WindowsSdkDir)Include\*))),$(if $(wildcard $(call fix_path,$(WindowsSdkDir)Include/$(folder)/um/Windows.h)),$(folder),)))$(BACKSLASH) + WindowsSDKVersion := $(WindowsSDKVersion) + + VsInstallBuildTools = $(ProgramFiles86)/Microsoft Visual Studio/2017/BuildTools + VsInstallEnterprise = $(ProgramFiles86)/Microsoft Visual Studio/2017/Enterprise + VsInstallProfessional = $(ProgramFiles86)/Microsoft Visual Studio/2017/Professional + VsInstallCommunity = $(ProgramFiles86)/Microsoft Visual Studio/2017/Community + + VsInstallRoot ?= $(shell if [ -d "$(VsInstallBuildTools)" ]; then echo "$(VsInstallBuildTools)"; fi) + ifeq ($(VsInstallRoot), ) + VsInstallRoot = $(shell if [ -d "$(VsInstallEnterprise)" ]; then echo "$(VsInstallEnterprise)"; fi) + endif + ifeq ($(VsInstallRoot), ) + VsInstallRoot = $(shell if [ -d "$(VsInstallProfessional)" ]; then echo "$(VsInstallProfessional)"; fi) + endif + ifeq ($(VsInstallRoot), ) + VsInstallRoot = $(shell if [ -d "$(VsInstallCommunity)" ]; then echo "$(VsInstallCommunity)"; fi) + endif + VsInstallRoot := $(VsInstallRoot) + + VcCompilerToolsVer := $(shell cat "$(VsInstallRoot)/VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt" | grep -o '[0-9\.]*') + VcCompilerToolsDir := $(VsInstallRoot)/VC/Tools/MSVC/$(VcCompilerToolsVer) + + WindowsSDKSharedIncludeDir := $(shell cygpath -w "$(WindowsSdkDir)\Include\$(WindowsSDKVersion)\shared") + WindowsSDKUCRTIncludeDir := $(shell cygpath -w "$(WindowsSdkDir)\Include\$(WindowsSDKVersion)\ucrt") + WindowsSDKUMIncludeDir := $(shell cygpath -w "$(WindowsSdkDir)\Include\$(WindowsSDKVersion)\um") + WindowsSDKUCRTLibDir := $(shell cygpath -w "$(WindowsSdkDir)\Lib\$(WindowsSDKVersion)\ucrt\$(TargetArchMoniker)") + WindowsSDKUMLibDir := $(shell cygpath -w "$(WindowsSdkDir)\Lib\$(WindowsSDKVersion)\um\$(TargetArchMoniker)") + + # For some reason the HostX86 compiler doesn't like compiling for x64 + # ("no such file" opening a shared library), and vice-versa. + # Work around it for now by using the strictly x86 compiler for x86, and x64 for x64. + # NOTE: What about ARM? + ifneq (,$(findstring x64,$(TargetArchMoniker))) + VCCompilerToolsBinDir := $(VcCompilerToolsDir)\bin\HostX64 + else + VCCompilerToolsBinDir := $(VcCompilerToolsDir)\bin\HostX86 + endif + + PATH := $(shell IFS=$$'\n'; cygpath "$(VCCompilerToolsBinDir)/$(TargetArchMoniker)"):$(PATH) + PATH := $(PATH):$(shell IFS=$$'\n'; cygpath "$(VsInstallRoot)/Common7/IDE") + INCLUDE := $(shell IFS=$$'\n'; cygpath -w "$(VcCompilerToolsDir)/include") + LIB := $(shell IFS=$$'\n'; cygpath -w "$(VcCompilerToolsDir)/lib/$(TargetArchMoniker)") + ifneq (,$(findstring uwp,$(PlatformSuffix))) + LIB := $(LIB);$(shell IFS=$$'\n'; cygpath -w "$(LIB)/store") + endif + + export INCLUDE := $(INCLUDE);$(WindowsSDKSharedIncludeDir);$(WindowsSDKUCRTIncludeDir);$(WindowsSDKUMIncludeDir) + export LIB := $(LIB);$(WindowsSDKUCRTLibDir);$(WindowsSDKUMLibDir) + TARGET := $(TARGET_NAME)_libretro.dll + PSS_STYLE :=2 + LDFLAGS += -DLL + +else + CC ?= gcc + TARGET := $(TARGET_NAME)_libretro.dll + SHARED := -shared -static-libgcc -static-libstdc++ -s -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined +endif + +TARGET := $(CORE_DIR)/build/bin/$(TARGET) + +# To force use of the Unix version instead of the Windows version +MKDIR := $(shell which mkdir) + +LDFLAGS += $(LIBM) + +ifeq ($(DEBUG), 1) + CFLAGS += -O0 -g +else + CFLAGS += -O2 -DNDEBUG +endif + +include Makefile.common + +CFLAGS += -DSAMEBOY_CORE_VERSION=\"$(VERSION)\" + +OBJECTS := $(patsubst $(CORE_DIR)/%.c,$(CORE_DIR)/build/obj/%_libretro.c.o,$(SOURCES_C)) + +OBJOUT = -o +LINKOUT = -o + +ifneq (,$(findstring msvc,$(platform))) + OBJOUT = -Fo + LINKOUT = -out: +ifeq ($(STATIC_LINKING),1) + LD ?= lib.exe + STATIC_LINKING=0 +else + LD = link.exe +endif +else + LD = $(CC) +endif + +CFLAGS += -D__LIBRETRO__ $(fpic) $(INCFLAGS) -std=gnu11 -D_GNU_SOURCE -D_USE_MATH_DEFINES + +all: $(TARGET) + +$(CORE_DIR)/libretro/%_boot.c: $(CORE_DIR)/build/bin/BootROMs/%_boot.bin + echo "/* AUTO-GENERATED */" > $@ + echo "const unsigned char $(notdir $(@:%.c=%))[] = {" >> $@ +ifneq ($(findstring Haiku,$(shell uname -s)),) + # turns out od is posix, hexdump is not hence is less portable + # this is still rather ugly and could be done better I guess + od -A none -t x1 -v $< | sed -e 's/^\ /0x/' -e 's/\ /,\ 0x/g' -e 's/$$/,/g' | tr '\n' ' ' >> $@ +else + hexdump -v -e '/1 "0x%02x, "' $< >> $@ +endif + echo "};" >> $@ + echo "const unsigned $(notdir $(@:%.c=%))_length = sizeof($(notdir $(@:%.c=%)));" >> $@ + +$(CORE_DIR)/build/bin/BootROMs/%_boot.bin: + $(MAKE) -C $(CORE_DIR) $(patsubst $(CORE_DIR)/%,%,$@) + +$(TARGET): $(OBJECTS) + -@$(MKDIR) -p $(dir $@) +ifeq ($(STATIC_LINKING), 1) + $(AR) rcs $@ $(OBJECTS) +else + $(LD) $(fpic) $(SHARED) $(INCFLAGS) $(LINKOUT)$@ $(OBJECTS) $(LDFLAGS) +endif + +$(CORE_DIR)/build/obj/%_libretro.c.o: $(CORE_DIR)/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) -c $(OBJOUT)$@ $< $(CFLAGS) $(fpic) -DGB_INTERNAL + +%.o: %.c + $(CC) $(CFLAGS) $(fpic) -c $(OBJOUT)$@ $< + +clean: + rm -f $(OBJECTS) $(TARGET) + +.PHONY: clean + diff --git a/bsnes/gb/libretro/Makefile.common b/bsnes/gb/libretro/Makefile.common new file mode 100644 index 00000000..fabe3ad4 --- /dev/null +++ b/bsnes/gb/libretro/Makefile.common @@ -0,0 +1,29 @@ +include $(CORE_DIR)/version.mk + +INCFLAGS := -I$(CORE_DIR) + +SOURCES_C := $(CORE_DIR)/Core/gb.c \ + $(CORE_DIR)/Core/sgb.c \ + $(CORE_DIR)/Core/apu.c \ + $(CORE_DIR)/Core/memory.c \ + $(CORE_DIR)/Core/mbc.c \ + $(CORE_DIR)/Core/timing.c \ + $(CORE_DIR)/Core/display.c \ + $(CORE_DIR)/Core/symbol_hash.c \ + $(CORE_DIR)/Core/camera.c \ + $(CORE_DIR)/Core/sm83_cpu.c \ + $(CORE_DIR)/Core/joypad.c \ + $(CORE_DIR)/Core/save_state.c \ + $(CORE_DIR)/Core/random.c \ + $(CORE_DIR)/Core/rumble.c \ + $(CORE_DIR)/libretro/agb_boot.c \ + $(CORE_DIR)/libretro/cgb_boot.c \ + $(CORE_DIR)/libretro/dmg_boot.c \ + $(CORE_DIR)/libretro/sgb_boot.c \ + $(CORE_DIR)/libretro/sgb2_boot.c \ + $(CORE_DIR)/libretro/libretro.c + +CFLAGS += -DGB_DISABLE_TIMEKEEPING -DGB_DISABLE_REWIND -DGB_DISABLE_DEBUGGER -DGB_DISABLE_CHEATS + + +SOURCES_CXX := diff --git a/bsnes/gb/libretro/jni/Android.mk b/bsnes/gb/libretro/jni/Android.mk new file mode 100644 index 00000000..e0646b9a --- /dev/null +++ b/bsnes/gb/libretro/jni/Android.mk @@ -0,0 +1,32 @@ +LOCAL_PATH := $(call my-dir) + +CORE_DIR := $(LOCAL_PATH)/../.. + +CFLAGS := + +include $(CORE_DIR)/libretro/Makefile.common + +GENERATED_SOURCES := $(filter %_boot.c,$(SOURCES_C)) + +COREFLAGS := -DINLINE=inline -D__LIBRETRO__ -DGB_INTERNAL $(INCFLAGS) -DSAMEBOY_CORE_VERSION=\"$(VERSION)\" -Wno-multichar -DANDROID + +GIT_VERSION := " $(shell git rev-parse --short HEAD || echo unknown)" +ifneq ($(GIT_VERSION)," unknown") + COREFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\" +endif + +include $(CLEAR_VARS) +LOCAL_MODULE := retro +LOCAL_SRC_FILES := $(SOURCES_C) +LOCAL_CFLAGS := -std=c99 $(COREFLAGS) $(CFLAGS) +LOCAL_LDFLAGS := -Wl,-version-script=$(CORE_DIR)/libretro/link.T +include $(BUILD_SHARED_LIBRARY) + +$(CORE_DIR)/libretro/%_boot.c: $(CORE_DIR)/build/bin/BootROMs/%_boot.bin + echo "/* AUTO-GENERATED */" > $@ + echo "const unsigned char $(notdir $(@:%.c=%))[] = {" >> $@ + hexdump -v -e '/1 "0x%02x, "' $< >> $@ + echo "};" >> $@ + echo "const unsigned $(notdir $(@:%.c=%))_length = sizeof($(notdir $(@:%.c=%)));" >> $@ + +.INTERMEDIATE: $(GENERATED_SOURCES) diff --git a/bsnes/gb/libretro/jni/Application.mk b/bsnes/gb/libretro/jni/Application.mk new file mode 100644 index 00000000..a252a72d --- /dev/null +++ b/bsnes/gb/libretro/jni/Application.mk @@ -0,0 +1 @@ +APP_ABI := all diff --git a/bsnes/gb/libretro/libretro.c b/bsnes/gb/libretro/libretro.c new file mode 100644 index 00000000..9e27f031 --- /dev/null +++ b/bsnes/gb/libretro/libretro.c @@ -0,0 +1,1378 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIIU +#define AUDIO_FREQUENCY 384000 +#else +/* Use the internal sample rate for the Wii U */ +#define AUDIO_FREQUENCY 48000 +#endif + +#ifdef _WIN32 +#include +#include +#define snprintf _snprintf +#endif + +#include +#include "libretro.h" + +#ifdef _WIN32 +static const char slash = '\\'; +#else +static const char slash = '/'; +#endif + +#define MAX_VIDEO_WIDTH 256 +#define MAX_VIDEO_HEIGHT 224 +#define MAX_VIDEO_PIXELS (MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT) + + +#define RETRO_MEMORY_GAMEBOY_1_SRAM ((1 << 8) | RETRO_MEMORY_SAVE_RAM) +#define RETRO_MEMORY_GAMEBOY_1_RTC ((2 << 8) | RETRO_MEMORY_RTC) +#define RETRO_MEMORY_GAMEBOY_2_SRAM ((3 << 8) | RETRO_MEMORY_SAVE_RAM) +#define RETRO_MEMORY_GAMEBOY_2_RTC ((3 << 8) | RETRO_MEMORY_RTC) + +#define RETRO_GAME_TYPE_GAMEBOY_LINK_2P 0x101 + +char battery_save_path[512]; +char symbols_path[512]; + +enum model { + MODEL_DMG, + MODEL_CGB, + MODEL_AGB, + MODEL_SGB, + MODEL_SGB2, + MODEL_AUTO +}; + +static const GB_model_t libretro_to_internal_model[] = +{ + [MODEL_DMG] = GB_MODEL_DMG_B, + [MODEL_CGB] = GB_MODEL_CGB_E, + [MODEL_AGB] = GB_MODEL_AGB, + [MODEL_SGB] = GB_MODEL_SGB, + [MODEL_SGB2] = GB_MODEL_SGB2 +}; + +enum screen_layout { + LAYOUT_TOP_DOWN, + LAYOUT_LEFT_RIGHT +}; + +enum audio_out { + GB_1, + GB_2 +}; + +static enum model model[2]; +static enum model auto_model = MODEL_CGB; + +static uint32_t *frame_buf = NULL; +static uint32_t *frame_buf_copy = NULL; +static struct retro_log_callback logging; +static retro_log_printf_t log_cb; + +static retro_video_refresh_t video_cb; +static retro_audio_sample_t audio_sample_cb; +static retro_input_poll_t input_poll_cb; +static retro_input_state_t input_state_cb; + +static bool libretro_supports_bitmasks = false; + +static unsigned emulated_devices = 1; +static bool initialized = false; +static unsigned screen_layout = 0; +static unsigned audio_out = 0; + +static bool geometry_updated = false; +static bool link_cable_emulation = false; +/*static bool infrared_emulation = false;*/ + +signed short soundbuf[1024 * 2]; + +char retro_system_directory[4096]; +char retro_save_directory[4096]; +char retro_game_path[4096]; + +GB_gameboy_t gameboy[2]; + +extern const unsigned char dmg_boot[], cgb_boot[], agb_boot[], sgb_boot[], sgb2_boot[]; +extern const unsigned dmg_boot_length, cgb_boot_length, agb_boot_length, sgb_boot_length, sgb2_boot_length; +bool vblank1_occurred = false, vblank2_occurred = false; + +static void fallback_log(enum retro_log_level level, const char *fmt, ...) +{ + (void)level; + va_list va; + va_start(va, fmt); + vfprintf(stderr, fmt, va); + va_end(va); +} + +static struct retro_rumble_interface rumble; + +static void GB_update_keys_status(GB_gameboy_t *gb, unsigned port) +{ + uint16_t joypad_bits = 0; + + input_poll_cb(); + + if (libretro_supports_bitmasks) { + joypad_bits = input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_MASK); + } + else { + unsigned j; + + for (j = 0; j < (RETRO_DEVICE_ID_JOYPAD_R3+1); j++) { + if (input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, j)) { + joypad_bits |= (1 << j); + } + } + } + + GB_set_key_state_for_player(gb, GB_KEY_RIGHT, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_RIGHT)); + GB_set_key_state_for_player(gb, GB_KEY_LEFT, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_LEFT)); + GB_set_key_state_for_player(gb, GB_KEY_UP, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_UP)); + GB_set_key_state_for_player(gb, GB_KEY_DOWN, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_DOWN)); + GB_set_key_state_for_player(gb, GB_KEY_A, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_A)); + GB_set_key_state_for_player(gb, GB_KEY_B, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_B)); + GB_set_key_state_for_player(gb, GB_KEY_SELECT, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_SELECT)); + GB_set_key_state_for_player(gb, GB_KEY_START, emulated_devices == 1 ? port : 0, + joypad_bits & (1 << RETRO_DEVICE_ID_JOYPAD_START)); + +} + +static void rumble_callback(GB_gameboy_t *gb, double amplitude) +{ + if (!rumble.set_rumble_state) return; + + if (gb == &gameboy[0]) { + rumble.set_rumble_state(0, RETRO_RUMBLE_STRONG, 65535 * amplitude); + } + else if (gb == &gameboy[1]) { + rumble.set_rumble_state(1, RETRO_RUMBLE_STRONG, 65535 * amplitude); + } +} + +static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) +{ + if ((audio_out == GB_1 && gb == &gameboy[0]) || + (audio_out == GB_2 && gb == &gameboy[1])) { + audio_sample_cb(sample->left, sample->right); + } +} + +static void vblank1(GB_gameboy_t *gb) +{ + vblank1_occurred = true; +} + +static void vblank2(GB_gameboy_t *gb) +{ + vblank2_occurred = true; +} + +static bool bit_to_send1 = true, bit_to_send2 = true; + +static void serial_start1(GB_gameboy_t *gb, bool bit_received) +{ + bit_to_send1 = bit_received; +} + +static bool serial_end1(GB_gameboy_t *gb) +{ + bool ret = GB_serial_get_data_bit(&gameboy[1]); + GB_serial_set_data_bit(&gameboy[1], bit_to_send1); + return ret; +} + +static void serial_start2(GB_gameboy_t *gb, bool bit_received) +{ + bit_to_send2 = bit_received; +} + +static bool serial_end2(GB_gameboy_t *gb) +{ + bool ret = GB_serial_get_data_bit(&gameboy[0]); + GB_serial_set_data_bit(&gameboy[0], bit_to_send2); + return ret; +} + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return r <<16 | g <<8 | b; +} + +static retro_environment_t environ_cb; + +/* variables for single cart mode */ +static const struct retro_variable vars_single[] = { + { "sameboy_color_correction_mode", "Color correction; emulate hardware|preserve brightness|reduce contrast|off|correct curves" }, + { "sameboy_high_pass_filter_mode", "High-pass filter; accurate|remove dc offset|off" }, + { "sameboy_model", "Emulated model (Restart game); Auto|Game Boy|Game Boy Color|Game Boy Advance|Super Game Boy|Super Game Boy 2" }, + { "sameboy_border", "Display border; Super Game Boy only|always|never" }, + { "sameboy_rumble", "Enable rumble; rumble-enabled games|all games|never" }, + { NULL } +}; + +/* variables for dual cart dual gameboy mode */ +static const struct retro_variable vars_dual[] = { + { "sameboy_link", "Link cable emulation; enabled|disabled" }, + /*{ "sameboy_ir", "Infrared Sensor Emulation; disabled|enabled" },*/ + { "sameboy_screen_layout", "Screen layout; top-down|left-right" }, + { "sameboy_audio_output", "Audio output; Game Boy #1|Game Boy #2" }, + { "sameboy_model_1", "Emulated model for Game Boy #1 (Restart game); Auto|Game Boy|Game Boy Color|Game Boy Advance" }, + { "sameboy_model_2", "Emulated model for Game Boy #2 (Restart game); Auto|Game Boy|Game Boy Color|Game Boy Advance" }, + { "sameboy_color_correction_mode_1", "Color correction for Game Boy #1; emulate hardware|preserve brightness|reduce contrast|off|correct curves" }, + { "sameboy_color_correction_mode_2", "Color correction for Game Boy #2; emulate hardware|preserve brightness|reduce contrast|off|correct curves" }, + { "sameboy_high_pass_filter_mode_1", "High-pass filter for Game Boy #1; accurate|remove dc offset|off" }, + { "sameboy_high_pass_filter_mode_2", "High-pass filter for Game Boy #2; accurate|remove dc offset|off" }, + { "sameboy_rumble_1", "Enable rumble for Game Boy #1; rumble-enabled games|all games|never" }, + { "sameboy_rumble_2", "Enable rumble for Game Boy #2; rumble-enabled games|all games|never" }, + { NULL } +}; + +static const struct retro_subsystem_memory_info gb1_memory[] = { + { "srm", RETRO_MEMORY_GAMEBOY_1_SRAM }, + { "rtc", RETRO_MEMORY_GAMEBOY_1_RTC }, +}; + +static const struct retro_subsystem_memory_info gb2_memory[] = { + { "srm", RETRO_MEMORY_GAMEBOY_2_SRAM }, + { "rtc", RETRO_MEMORY_GAMEBOY_2_RTC }, +}; + +static const struct retro_subsystem_rom_info gb_roms[] = { + { "GameBoy #1", "gb|gbc", true, false, true, gb1_memory, 1 }, + { "GameBoy #2", "gb|gbc", true, false, true, gb2_memory, 1 }, +}; + +static const struct retro_subsystem_info subsystems[] = { + { "2 Player Game Boy Link", "gb_link_2p", gb_roms, 2, RETRO_GAME_TYPE_GAMEBOY_LINK_2P }, + { NULL }, +}; + +static const struct retro_controller_description controllers[] = { + { "Nintendo Game Boy", RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 0) }, +}; + +static const struct retro_controller_description controllers_sgb[] = { + { "SNES/SFC Gamepad", RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 0) }, +}; + +static struct retro_input_descriptor descriptors_1p[] = { + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 0 }, +}; + +static struct retro_input_descriptor descriptors_2p[] = { + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 0 }, +}; + +static struct retro_input_descriptor descriptors_4p[] = { + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 1, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 2, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 3, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 0 }, +}; + + +static void set_link_cable_state(bool state) +{ + if (state && emulated_devices == 2) { + GB_set_serial_transfer_bit_start_callback(&gameboy[0], serial_start1); + GB_set_serial_transfer_bit_end_callback(&gameboy[0], serial_end1); + GB_set_serial_transfer_bit_start_callback(&gameboy[1], serial_start2); + GB_set_serial_transfer_bit_end_callback(&gameboy[1], serial_end2); + } + else if (!state) { + GB_set_serial_transfer_bit_start_callback(&gameboy[0], NULL); + GB_set_serial_transfer_bit_end_callback(&gameboy[0], NULL); + GB_set_serial_transfer_bit_start_callback(&gameboy[1], NULL); + GB_set_serial_transfer_bit_end_callback(&gameboy[1], NULL); + } +} + +static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) +{ + const char *model_name = (char *[]){ + [GB_BOOT_ROM_DMG0] = "dmg0", + [GB_BOOT_ROM_DMG] = "dmg", + [GB_BOOT_ROM_MGB] = "mgb", + [GB_BOOT_ROM_SGB] = "sgb", + [GB_BOOT_ROM_SGB2] = "sgb2", + [GB_BOOT_ROM_CGB0] = "cgb0", + [GB_BOOT_ROM_CGB] = "cgb", + [GB_BOOT_ROM_AGB] = "agb", + }[type]; + + const uint8_t *boot_code = (const unsigned char *[]) + { + [GB_BOOT_ROM_DMG0] = dmg_boot, // dmg0 not implemented yet + [GB_BOOT_ROM_DMG] = dmg_boot, + [GB_BOOT_ROM_MGB] = dmg_boot, // mgb not implemented yet + [GB_BOOT_ROM_SGB] = sgb_boot, + [GB_BOOT_ROM_SGB2] = sgb2_boot, + [GB_BOOT_ROM_CGB0] = cgb_boot, // cgb0 not implemented yet + [GB_BOOT_ROM_CGB] = cgb_boot, + [GB_BOOT_ROM_AGB] = agb_boot, + }[type]; + + unsigned boot_length = (unsigned []){ + [GB_BOOT_ROM_DMG0] = dmg_boot_length, // dmg0 not implemented yet + [GB_BOOT_ROM_DMG] = dmg_boot_length, + [GB_BOOT_ROM_MGB] = dmg_boot_length, // mgb not implemented yet + [GB_BOOT_ROM_SGB] = sgb_boot_length, + [GB_BOOT_ROM_SGB2] = sgb2_boot_length, + [GB_BOOT_ROM_CGB0] = cgb_boot_length, // cgb0 not implemented yet + [GB_BOOT_ROM_CGB] = cgb_boot_length, + [GB_BOOT_ROM_AGB] = agb_boot_length, + }[type]; + + char buf[256]; + snprintf(buf, sizeof(buf), "%s%c%s_boot.bin", retro_system_directory, slash, model_name); + log_cb(RETRO_LOG_INFO, "Initializing as model: %s\n", model_name); + log_cb(RETRO_LOG_INFO, "Loading boot image: %s\n", buf); + + if (GB_load_boot_rom(gb, buf)) { + GB_load_boot_rom_from_buffer(gb, boot_code, boot_length); + } +} + +static void retro_set_memory_maps(void) +{ + struct retro_memory_descriptor descs[11]; + size_t size; + uint16_t bank; + unsigned i; + + + /* todo: add netplay awareness for this so achievements can be granted on the respective client */ + i = 0; + memset(descs, 0, sizeof(descs)); + + descs[0].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_IE, &size, &bank); + descs[0].start = 0xFFFF; + descs[0].len = 1; + + descs[1].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_HRAM, &size, &bank); + descs[1].start = 0xFF80; + descs[1].len = 0x0080; + + descs[2].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_RAM, &size, &bank); + descs[2].start = 0xC000; + descs[2].len = 0x1000; + + descs[3].ptr = descs[2].ptr + 0x1000; /* GB RAM/GBC RAM bank 1 */ + descs[3].start = 0xD000; + descs[3].len = 0x1000; + + descs[4].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_CART_RAM, &size, &bank); + descs[4].start = 0xA000; + descs[4].len = 0x2000; + + descs[5].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_VRAM, &size, &bank); + descs[5].start = 0x8000; + descs[5].len = 0x2000; + + descs[6].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_ROM, &size, &bank); + descs[6].start = 0x0000; + descs[6].len = 0x4000; + descs[6].flags = RETRO_MEMDESC_CONST; + + descs[7].ptr = descs[6].ptr + (bank * 0x4000); + descs[7].start = 0x4000; + descs[7].len = 0x4000; + descs[7].flags = RETRO_MEMDESC_CONST; + + descs[8].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_OAM, &size, &bank); + descs[8].start = 0xFE00; + descs[8].len = 0x00A0; + descs[8].select = 0xFFFFFF00; + + descs[9].ptr = descs[2].ptr + 0x2000; /* GBC RAM bank 2 */ + descs[9].start = 0x10000; + descs[9].len = GB_is_cgb(&gameboy[i]) ? 0x6000 : 0; /* 0x1000 per bank (2-7), unmapped on GB */ + descs[9].select = 0xFFFF0000; + + descs[10].ptr = GB_get_direct_access(&gameboy[i], GB_DIRECT_ACCESS_IO, &size, &bank); + descs[10].start = 0xFF00; + descs[10].len = 0x0080; + descs[10].select = 0xFFFFFF00; + + struct retro_memory_map mmaps; + mmaps.descriptors = descs; + mmaps.num_descriptors = sizeof(descs) / sizeof(descs[0]); + environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, &mmaps); +} + +static void init_for_current_model(unsigned id) +{ + unsigned i = id; + enum model effective_model; + + effective_model = model[i]; + if (effective_model == MODEL_AUTO) { + effective_model = auto_model; + } + + + if (GB_is_inited(&gameboy[i])) { + GB_switch_model_and_reset(&gameboy[i], libretro_to_internal_model[effective_model]); + } + else { + GB_init(&gameboy[i], libretro_to_internal_model[effective_model]); + } + + GB_set_boot_rom_load_callback(&gameboy[i], boot_rom_load); + + /* When running multiple devices they are assumed to use the same resolution */ + + GB_set_pixels_output(&gameboy[i], + (uint32_t *)(frame_buf + GB_get_screen_width(&gameboy[0]) * GB_get_screen_height(&gameboy[0]) * i)); + GB_set_rgb_encode_callback(&gameboy[i], rgb_encode); + GB_set_sample_rate(&gameboy[i], AUDIO_FREQUENCY); + GB_apu_set_sample_callback(&gameboy[i], audio_callback); + GB_set_rumble_callback(&gameboy[i], rumble_callback); + + /* todo: attempt to make these more generic */ + GB_set_vblank_callback(&gameboy[0], (GB_vblank_callback_t) vblank1); + if (emulated_devices == 2) { + GB_set_vblank_callback(&gameboy[1], (GB_vblank_callback_t) vblank2); + if (link_cable_emulation) { + set_link_cable_state(true); + } + } + + /* Let's be extremely nitpicky about how devices and descriptors are set */ + if (emulated_devices == 1 && (model[0] == MODEL_SGB || model[0] == MODEL_SGB2)) { + static const struct retro_controller_info ports[] = { + { controllers_sgb, 1 }, + { controllers_sgb, 1 }, + { controllers_sgb, 1 }, + { controllers_sgb, 1 }, + { NULL, 0 }, + }; + environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); + environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, descriptors_4p); + } + else if (emulated_devices == 1) { + static const struct retro_controller_info ports[] = { + { controllers, 1 }, + { NULL, 0 }, + }; + environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); + environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, descriptors_1p); + } + else { + static const struct retro_controller_info ports[] = { + { controllers, 1 }, + { controllers, 1 }, + { NULL, 0 }, + }; + environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); + environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, descriptors_2p); + } + +} + +static void check_variables() +{ + struct retro_variable var = {0}; + if (emulated_devices == 1) { + var.key = "sameboy_color_correction_mode"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_DISABLED); + } + else if (strcmp(var.value, "correct curves") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_CORRECT_CURVES); + } + else if (strcmp(var.value, "emulate hardware") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + } + else if (strcmp(var.value, "preserve brightness") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + } + else if (strcmp(var.value, "reduce contrast") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_REDUCE_CONTRAST); + } + } + + var.key = "sameboy_rumble"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "never") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_DISABLED); + } + else if (strcmp(var.value, "rumble-enabled games") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_CARTRIDGE_ONLY); + } + else if (strcmp(var.value, "all games") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_ALL_GAMES); + } + } + + var.key = "sameboy_high_pass_filter_mode"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_OFF); + } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_ACCURATE); + } + else if (strcmp(var.value, "remove dc offset") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_REMOVE_DC_OFFSET); + } + } + + var.key = "sameboy_model"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + enum model new_model = model[0]; + if (strcmp(var.value, "Game Boy") == 0) { + new_model = MODEL_DMG; + } + else if (strcmp(var.value, "Game Boy Color") == 0) { + new_model = MODEL_CGB; + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { + new_model = MODEL_AGB; + } + else if (strcmp(var.value, "Super Game Boy") == 0) { + new_model = MODEL_SGB; + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { + new_model = MODEL_SGB2; + } + else { + new_model = MODEL_AUTO; + } + + model[0] = new_model; + } + + var.key = "sameboy_border"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "never") == 0) { + GB_set_border_mode(&gameboy[0], GB_BORDER_NEVER); + } + else if (strcmp(var.value, "Super Game Boy only") == 0) { + GB_set_border_mode(&gameboy[0], GB_BORDER_SGB); + } + else if (strcmp(var.value, "always") == 0) { + GB_set_border_mode(&gameboy[0], GB_BORDER_ALWAYS); + } + + geometry_updated = true; + } + } + else { + GB_set_border_mode(&gameboy[0], GB_BORDER_NEVER); + GB_set_border_mode(&gameboy[1], GB_BORDER_NEVER); + var.key = "sameboy_color_correction_mode_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_DISABLED); + } + else if (strcmp(var.value, "correct curves") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_CORRECT_CURVES); + } + else if (strcmp(var.value, "emulate hardware") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + } + else if (strcmp(var.value, "preserve brightness") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + } + else if (strcmp(var.value, "reduce contrast") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_REDUCE_CONTRAST); + } + } + + var.key = "sameboy_color_correction_mode_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_DISABLED); + } + else if (strcmp(var.value, "correct curves") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_CORRECT_CURVES); + } + else if (strcmp(var.value, "emulate hardware") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + } + else if (strcmp(var.value, "preserve brightness") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + } + else if (strcmp(var.value, "reduce contrast") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_REDUCE_CONTRAST); + } + + } + + var.key = "sameboy_rumble_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "never") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_DISABLED); + } + else if (strcmp(var.value, "rumble-enabled games") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_CARTRIDGE_ONLY); + } + else if (strcmp(var.value, "all games") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_ALL_GAMES); + } + } + + var.key = "sameboy_rumble_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "never") == 0) { + GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_DISABLED); + } + else if (strcmp(var.value, "rumble-enabled games") == 0) { + GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_CARTRIDGE_ONLY); + } + else if (strcmp(var.value, "all games") == 0) { + GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_ALL_GAMES); + } + } + + var.key = "sameboy_high_pass_filter_mode_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_OFF); + } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_ACCURATE); + } + else if (strcmp(var.value, "remove dc offset") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_REMOVE_DC_OFFSET); + } + } + + var.key = "sameboy_high_pass_filter_mode_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_OFF); + } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_ACCURATE); + } + else if (strcmp(var.value, "remove dc offset") == 0) { + GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_REMOVE_DC_OFFSET); + } + } + + var.key = "sameboy_model_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + enum model new_model = model[0]; + if (strcmp(var.value, "Game Boy") == 0) { + new_model = MODEL_DMG; + } + else if (strcmp(var.value, "Game Boy Color") == 0) { + new_model = MODEL_CGB; + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { + new_model = MODEL_AGB; + } + else if (strcmp(var.value, "Super Game Boy") == 0) { + new_model = MODEL_SGB; + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { + new_model = MODEL_SGB2; + } + else { + new_model = MODEL_AUTO; + } + + model[0] = new_model; + } + + var.key = "sameboy_model_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + enum model new_model = model[1]; + if (strcmp(var.value, "Game Boy") == 0) { + new_model = MODEL_DMG; + } + else if (strcmp(var.value, "Game Boy Color") == 0) { + new_model = MODEL_CGB; + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { + new_model = MODEL_AGB; + } + else if (strcmp(var.value, "Super Game Boy") == 0) { + new_model = MODEL_SGB; + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { + new_model = MODEL_SGB; + } + else { + new_model = MODEL_AUTO; + } + + model[1] = new_model; + } + + var.key = "sameboy_screen_layout"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "top-down") == 0) { + screen_layout = LAYOUT_TOP_DOWN; + } + else { + screen_layout = LAYOUT_LEFT_RIGHT; + } + + geometry_updated = true; + } + + var.key = "sameboy_link"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + bool tmp = link_cable_emulation; + if (strcmp(var.value, "enabled") == 0) { + link_cable_emulation = true; + } + else { + link_cable_emulation = false; + } + if (link_cable_emulation && link_cable_emulation != tmp) { + set_link_cable_state(true); + } + else if (!link_cable_emulation && link_cable_emulation != tmp) { + set_link_cable_state(false); + } + } + + var.key = "sameboy_audio_output"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "Game Boy #1") == 0) { + audio_out = GB_1; + } + else { + audio_out = GB_2; + } + } + } +} + +void retro_init(void) +{ + const char *dir = NULL; + + if (environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &dir) && dir) { + snprintf(retro_system_directory, sizeof(retro_system_directory), "%s", dir); + } + else { + snprintf(retro_system_directory, sizeof(retro_system_directory), "%s", "."); + } + + if (environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &dir) && dir) { + snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", dir); + } + else { + snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", "."); + } + + if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &logging)) { + log_cb = logging.log; + } + else { + log_cb = fallback_log; + } + + if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL)) { + libretro_supports_bitmasks = true; + } +} + +void retro_deinit(void) +{ + free(frame_buf); + free(frame_buf_copy); + frame_buf = NULL; + frame_buf_copy = NULL; + + libretro_supports_bitmasks = false; +} + +unsigned retro_api_version(void) +{ + return RETRO_API_VERSION; +} + +void retro_set_controller_port_device(unsigned port, unsigned device) +{ + log_cb(RETRO_LOG_INFO, "Connecting device %u into port %u\n", device, port); +} + +void retro_get_system_info(struct retro_system_info *info) +{ + memset(info, 0, sizeof(*info)); + info->library_name = "SameBoy"; +#ifdef GIT_VERSION + info->library_version = SAMEBOY_CORE_VERSION GIT_VERSION; +#else + info->library_version = SAMEBOY_CORE_VERSION; +#endif + info->need_fullpath = true; + info->valid_extensions = "gb|gbc"; +} + +void retro_get_system_av_info(struct retro_system_av_info *info) +{ + struct retro_game_geometry geom; + struct retro_system_timing timing = { GB_get_usual_frame_rate(&gameboy[0]), AUDIO_FREQUENCY }; + + if (emulated_devices == 2) { + if (screen_layout == LAYOUT_TOP_DOWN) { + geom.base_width = GB_get_screen_width(&gameboy[0]); + geom.base_height = GB_get_screen_height(&gameboy[0]) * emulated_devices; + geom.aspect_ratio = (double)GB_get_screen_width(&gameboy[0]) / (emulated_devices * GB_get_screen_height(&gameboy[0])); + } + else if (screen_layout == LAYOUT_LEFT_RIGHT) { + geom.base_width = GB_get_screen_width(&gameboy[0]) * emulated_devices; + geom.base_height = GB_get_screen_height(&gameboy[0]); + geom.aspect_ratio = ((double)GB_get_screen_width(&gameboy[0]) * emulated_devices) / GB_get_screen_height(&gameboy[0]); + } + } + else { + geom.base_width = GB_get_screen_width(&gameboy[0]); + geom.base_height = GB_get_screen_height(&gameboy[0]); + geom.aspect_ratio = (double)GB_get_screen_width(&gameboy[0]) / GB_get_screen_height(&gameboy[0]); + } + + geom.max_width = MAX_VIDEO_WIDTH * emulated_devices; + geom.max_height = MAX_VIDEO_HEIGHT * emulated_devices; + + info->geometry = geom; + info->timing = timing; +} + + +void retro_set_environment(retro_environment_t cb) +{ + environ_cb = cb; + + cb(RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO, (void*)subsystems); +} + +void retro_set_audio_sample(retro_audio_sample_t cb) +{ + audio_sample_cb = cb; +} + +void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) +{ +} + +void retro_set_input_poll(retro_input_poll_t cb) +{ + input_poll_cb = cb; +} + +void retro_set_input_state(retro_input_state_t cb) +{ + input_state_cb = cb; +} + +void retro_set_video_refresh(retro_video_refresh_t cb) +{ + video_cb = cb; +} + +void retro_reset(void) +{ + check_variables(); + + for (int i = 0; i < emulated_devices; i++) { + init_for_current_model(i); + GB_reset(&gameboy[i]); + } + + geometry_updated = true; +} + +void retro_run(void) +{ + + bool updated = false; + + if (!initialized) { + geometry_updated = false; + } + + if (geometry_updated) { + struct retro_system_av_info info; + retro_get_system_av_info(&info); + environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, &info.geometry); + geometry_updated = false; + } + + if (!frame_buf) { + return; + } + + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated) { + check_variables(); + } + + if (emulated_devices == 2) { + GB_update_keys_status(&gameboy[0], 0); + GB_update_keys_status(&gameboy[1], 1); + } + else if (emulated_devices == 1 && (model[0] == MODEL_SGB || model[0] == MODEL_SGB2)) { + for (unsigned i = 0; i < 4; i++) { + GB_update_keys_status(&gameboy[0], i); + } + } + else { + GB_update_keys_status(&gameboy[0], 0); + } + + vblank1_occurred = vblank2_occurred = false; + signed delta = 0; + if (emulated_devices == 2) { + while (!vblank1_occurred || !vblank2_occurred) { + if (delta >= 0) { + delta -= GB_run(&gameboy[0]); + } + else { + delta += GB_run(&gameboy[1]); + } + } + } + else { + GB_run_frame(&gameboy[0]); + } + + if (emulated_devices == 2) { + if (screen_layout == LAYOUT_TOP_DOWN) { + video_cb(frame_buf, + GB_get_screen_width(&gameboy[0]), + GB_get_screen_height(&gameboy[0]) * emulated_devices, + GB_get_screen_width(&gameboy[0]) * sizeof(uint32_t)); + } + else if (screen_layout == LAYOUT_LEFT_RIGHT) { + unsigned pitch = GB_get_screen_width(&gameboy[0]) * emulated_devices; + unsigned pixels_per_device = GB_get_screen_width(&gameboy[0]) * GB_get_screen_height(&gameboy[0]); + for (int y = 0; y < GB_get_screen_height(&gameboy[0]); y++) { + for (unsigned i = 0; i < emulated_devices; i++) { + memcpy(frame_buf_copy + y * pitch + GB_get_screen_width(&gameboy[0]) * i, + frame_buf + pixels_per_device * i + y * GB_get_screen_width(&gameboy[0]), + GB_get_screen_width(&gameboy[0]) * sizeof(uint32_t)); + } + } + + video_cb(frame_buf_copy, GB_get_screen_width(&gameboy[0]) * emulated_devices, GB_get_screen_height(&gameboy[0]), GB_get_screen_width(&gameboy[0]) * emulated_devices * sizeof(uint32_t)); + } + } + else { + video_cb(frame_buf, + GB_get_screen_width(&gameboy[0]), + GB_get_screen_height(&gameboy[0]), + GB_get_screen_width(&gameboy[0]) * sizeof(uint32_t)); + } + + + initialized = true; +} + +bool retro_load_game(const struct retro_game_info *info) +{ + environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void *)vars_single); + check_variables(); + + frame_buf = (uint32_t *)malloc(MAX_VIDEO_PIXELS * emulated_devices * sizeof(uint32_t)); + memset(frame_buf, 0, MAX_VIDEO_PIXELS * emulated_devices * sizeof(uint32_t)); + + enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888; + if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { + log_cb(RETRO_LOG_INFO, "XRGB8888 is not supported\n"); + return false; + } + + auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB : MODEL_DMG; + snprintf(retro_game_path, sizeof(retro_game_path), "%s", info->path); + + for (int i = 0; i < emulated_devices; i++) { + init_for_current_model(i); + if (GB_load_rom(&gameboy[i], info->path)) { + log_cb(RETRO_LOG_INFO, "Failed to load ROM at %s\n", info->path); + return false; + } + } + + bool achievements = true; + environ_cb(RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS, &achievements); + + if (environ_cb(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumble)) { + log_cb(RETRO_LOG_INFO, "Rumble environment supported\n"); + } + else { + log_cb(RETRO_LOG_INFO, "Rumble environment not supported\n"); + } + + check_variables(); + + retro_set_memory_maps(); + + return true; +} + +void retro_unload_game(void) +{ + for (int i = 0; i < emulated_devices; i++) { + GB_free(&gameboy[i]); + } +} + +unsigned retro_get_region(void) +{ + return RETRO_REGION_NTSC; +} + +bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t num_info) +{ + + if (type == RETRO_GAME_TYPE_GAMEBOY_LINK_2P) { + emulated_devices = 2; + } + else { + return false; /* all other types are unhandled for now */ + } + + environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void *)vars_dual); + check_variables(); + + frame_buf = (uint32_t*)malloc(emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); + frame_buf_copy = (uint32_t*)malloc(emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); + + memset(frame_buf, 0, emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); + memset(frame_buf_copy, 0, emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); + + enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888; + if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { + log_cb(RETRO_LOG_INFO, "XRGB8888 is not supported\n"); + return false; + } + + auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB : MODEL_DMG; + snprintf(retro_game_path, sizeof(retro_game_path), "%s", info->path); + + for (int i = 0; i < emulated_devices; i++) { + init_for_current_model(i); + if (GB_load_rom(&gameboy[i], info[i].path)) { + log_cb(RETRO_LOG_INFO, "Failed to load ROM\n"); + return false; + } + } + + bool achievements = true; + environ_cb(RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS, &achievements); + + if (environ_cb(RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE, &rumble)) { + log_cb(RETRO_LOG_INFO, "Rumble environment supported\n"); + } + else { + log_cb(RETRO_LOG_INFO, "Rumble environment not supported\n"); + } + + check_variables(); + return true; +} + +size_t retro_serialize_size(void) +{ + static size_t maximum_save_size = 0; + if (maximum_save_size) { + return maximum_save_size * 2; + } + + GB_gameboy_t temp; + + GB_init(&temp, GB_MODEL_DMG_B); + maximum_save_size = GB_get_save_state_size(&temp); + GB_free(&temp); + + GB_init(&temp, GB_MODEL_CGB_E); + maximum_save_size = MAX(maximum_save_size, GB_get_save_state_size(&temp)); + GB_free(&temp); + + GB_init(&temp, GB_MODEL_SGB2); + maximum_save_size = MAX(maximum_save_size, GB_get_save_state_size(&temp)); + GB_free(&temp); + + return maximum_save_size * 2; +} + +bool retro_serialize(void *data, size_t size) +{ + + if (!initialized || !data) { + return false; + } + + size_t offset = 0; + + for (int i = 0; i < emulated_devices; i++) { + size_t state_size = GB_get_save_state_size(&gameboy[i]); + if (state_size > size) { + return false; + } + + GB_save_state_to_buffer(&gameboy[i], ((uint8_t *) data) + offset); + offset += state_size; + size -= state_size; + } + + return true; +} + +bool retro_unserialize(const void *data, size_t size) +{ + for (int i = 0; i < emulated_devices; i++) { + size_t state_size = GB_get_save_state_size(&gameboy[i]); + if (state_size > size) { + return false; + } + + if (GB_load_state_from_buffer(&gameboy[i], data, state_size)) { + return false; + } + + size -= state_size; + data = ((uint8_t *)data) + state_size; + } + + return true; + +} + +void *retro_get_memory_data(unsigned type) +{ + void *data = NULL; + if (emulated_devices == 1) { + switch (type) { + case RETRO_MEMORY_SYSTEM_RAM: + data = gameboy[0].ram; + break; + case RETRO_MEMORY_SAVE_RAM: + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { + data = gameboy[0].mbc_ram; + } + else { + data = NULL; + } + break; + case RETRO_MEMORY_VIDEO_RAM: + data = gameboy[0].vram; + break; + case RETRO_MEMORY_RTC: + if (gameboy[0].cartridge_type->has_battery) { + data = GB_GET_SECTION(&gameboy[0], rtc); + } + else { + data = NULL; + } + break; + default: + break; + } + } + else { + switch (type) { + case RETRO_MEMORY_GAMEBOY_1_SRAM: + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { + data = gameboy[0].mbc_ram; + } + else { + data = NULL; + } + break; + case RETRO_MEMORY_GAMEBOY_2_SRAM: + if (gameboy[1].cartridge_type->has_battery && gameboy[1].mbc_ram_size != 0) { + data = gameboy[1].mbc_ram; + } + else { + data = NULL; + } + break; + case RETRO_MEMORY_GAMEBOY_1_RTC: + if (gameboy[0].cartridge_type->has_battery) { + data = GB_GET_SECTION(&gameboy[0], rtc); + } + else { + data = NULL; + } + break; + case RETRO_MEMORY_GAMEBOY_2_RTC: + if (gameboy[1].cartridge_type->has_battery) { + data = GB_GET_SECTION(&gameboy[1], rtc); + } + else { + data = NULL; + } + break; + default: + break; + } + } + + return data; +} + +size_t retro_get_memory_size(unsigned type) +{ + size_t size = 0; + if (emulated_devices == 1) { + switch (type) { + case RETRO_MEMORY_SYSTEM_RAM: + size = gameboy[0].ram_size; + break; + case RETRO_MEMORY_SAVE_RAM: + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { + size = gameboy[0].mbc_ram_size; + } + else { + size = 0; + } + break; + case RETRO_MEMORY_VIDEO_RAM: + size = gameboy[0].vram_size; + break; + case RETRO_MEMORY_RTC: + if (gameboy[0].cartridge_type->has_battery) { + size = GB_SECTION_SIZE(rtc); + } + else { + size = 0; + } + break; + default: + break; + } + } + else { + switch (type) { + case RETRO_MEMORY_GAMEBOY_1_SRAM: + if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { + size = gameboy[0].mbc_ram_size; + } + else { + size = 0; + } + break; + case RETRO_MEMORY_GAMEBOY_2_SRAM: + if (gameboy[1].cartridge_type->has_battery && gameboy[1].mbc_ram_size != 0) { + size = gameboy[1].mbc_ram_size; + } + else { + size = 0; + } + break; + case RETRO_MEMORY_GAMEBOY_1_RTC: + if (gameboy[0].cartridge_type->has_battery) { + size = GB_SECTION_SIZE(rtc); + } + break; + case RETRO_MEMORY_GAMEBOY_2_RTC: + if (gameboy[1].cartridge_type->has_battery) { + size = GB_SECTION_SIZE(rtc); + } + break; + default: + break; + } + } + + return size; +} + +void retro_cheat_reset(void) +{} + +void retro_cheat_set(unsigned index, bool enabled, const char *code) +{ + (void)index; + (void)enabled; + (void)code; +} + diff --git a/bsnes/gb/libretro/libretro.h b/bsnes/gb/libretro/libretro.h new file mode 100644 index 00000000..1fd2f5b7 --- /dev/null +++ b/bsnes/gb/libretro/libretro.h @@ -0,0 +1,2752 @@ +/* Copyright (C) 2010-2018 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this libretro API header (libretro.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef LIBRETRO_H__ +#define LIBRETRO_H__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef __cplusplus +#if defined(_MSC_VER) && _MSC_VER < 1800 && !defined(SN_TARGET_PS3) +/* Hack applied for MSVC when compiling in C89 mode + * as it isn't C99-compliant. */ +#define bool unsigned char +#define true 1 +#define false 0 +#else +#include +#endif +#endif + +#ifndef RETRO_CALLCONV +# if defined(__GNUC__) && defined(__i386__) && !defined(__x86_64__) +# define RETRO_CALLCONV __attribute__((cdecl)) +# elif defined(_MSC_VER) && defined(_M_X86) && !defined(_M_X64) +# define RETRO_CALLCONV __cdecl +# else +# define RETRO_CALLCONV /* all other platforms only have one calling convention each */ +# endif +#endif + +#ifndef RETRO_API +# if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) +# ifdef RETRO_IMPORT_SYMBOLS +# ifdef __GNUC__ +# define RETRO_API RETRO_CALLCONV __attribute__((__dllimport__)) +# else +# define RETRO_API RETRO_CALLCONV __declspec(dllimport) +# endif +# else +# ifdef __GNUC__ +# define RETRO_API RETRO_CALLCONV __attribute__((__dllexport__)) +# else +# define RETRO_API RETRO_CALLCONV __declspec(dllexport) +# endif +# endif +# else +# if defined(__GNUC__) && __GNUC__ >= 4 && !defined(__CELLOS_LV2__) +# define RETRO_API RETRO_CALLCONV __attribute__((__visibility__("default"))) +# else +# define RETRO_API RETRO_CALLCONV +# endif +# endif +#endif + +/* Used for checking API/ABI mismatches that can break libretro + * implementations. + * It is not incremented for compatible changes to the API. + */ +#define RETRO_API_VERSION 1 + +/* + * Libretro's fundamental device abstractions. + * + * Libretro's input system consists of some standardized device types, + * such as a joypad (with/without analog), mouse, keyboard, lightgun + * and a pointer. + * + * The functionality of these devices are fixed, and individual cores + * map their own concept of a controller to libretro's abstractions. + * This makes it possible for frontends to map the abstract types to a + * real input device, and not having to worry about binding input + * correctly to arbitrary controller layouts. + */ + +#define RETRO_DEVICE_TYPE_SHIFT 8 +#define RETRO_DEVICE_MASK ((1 << RETRO_DEVICE_TYPE_SHIFT) - 1) +#define RETRO_DEVICE_SUBCLASS(base, id) (((id + 1) << RETRO_DEVICE_TYPE_SHIFT) | base) + +/* Input disabled. */ +#define RETRO_DEVICE_NONE 0 + +/* The JOYPAD is called RetroPad. It is essentially a Super Nintendo + * controller, but with additional L2/R2/L3/R3 buttons, similar to a + * PS1 DualShock. */ +#define RETRO_DEVICE_JOYPAD 1 + +/* The mouse is a simple mouse, similar to Super Nintendo's mouse. + * X and Y coordinates are reported relatively to last poll (poll callback). + * It is up to the libretro implementation to keep track of where the mouse + * pointer is supposed to be on the screen. + * The frontend must make sure not to interfere with its own hardware + * mouse pointer. + */ +#define RETRO_DEVICE_MOUSE 2 + +/* KEYBOARD device lets one poll for raw key pressed. + * It is poll based, so input callback will return with the current + * pressed state. + * For event/text based keyboard input, see + * RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK. + */ +#define RETRO_DEVICE_KEYBOARD 3 + +/* LIGHTGUN device is similar to Guncon-2 for PlayStation 2. + * It reports X/Y coordinates in screen space (similar to the pointer) + * in the range [-0x8000, 0x7fff] in both axes, with zero being center and + * -0x8000 being out of bounds. + * As well as reporting on/off screen state. It features a trigger, + * start/select buttons, auxiliary action buttons and a + * directional pad. A forced off-screen shot can be requested for + * auto-reloading function in some games. + */ +#define RETRO_DEVICE_LIGHTGUN 4 + +/* The ANALOG device is an extension to JOYPAD (RetroPad). + * Similar to DualShock2 it adds two analog sticks and all buttons can + * be analog. This is treated as a separate device type as it returns + * axis values in the full analog range of [-0x7fff, 0x7fff], + * although some devices may return -0x8000. + * Positive X axis is right. Positive Y axis is down. + * Buttons are returned in the range [0, 0x7fff]. + * Only use ANALOG type when polling for analog values. + */ +#define RETRO_DEVICE_ANALOG 5 + +/* Abstracts the concept of a pointing mechanism, e.g. touch. + * This allows libretro to query in absolute coordinates where on the + * screen a mouse (or something similar) is being placed. + * For a touch centric device, coordinates reported are the coordinates + * of the press. + * + * Coordinates in X and Y are reported as: + * [-0x7fff, 0x7fff]: -0x7fff corresponds to the far left/top of the screen, + * and 0x7fff corresponds to the far right/bottom of the screen. + * The "screen" is here defined as area that is passed to the frontend and + * later displayed on the monitor. + * + * The frontend is free to scale/resize this screen as it sees fit, however, + * (X, Y) = (-0x7fff, -0x7fff) will correspond to the top-left pixel of the + * game image, etc. + * + * To check if the pointer coordinates are valid (e.g. a touch display + * actually being touched), PRESSED returns 1 or 0. + * + * If using a mouse on a desktop, PRESSED will usually correspond to the + * left mouse button, but this is a frontend decision. + * PRESSED will only return 1 if the pointer is inside the game screen. + * + * For multi-touch, the index variable can be used to successively query + * more presses. + * If index = 0 returns true for _PRESSED, coordinates can be extracted + * with _X, _Y for index = 0. One can then query _PRESSED, _X, _Y with + * index = 1, and so on. + * Eventually _PRESSED will return false for an index. No further presses + * are registered at this point. */ +#define RETRO_DEVICE_POINTER 6 + +/* Buttons for the RetroPad (JOYPAD). + * The placement of these is equivalent to placements on the + * Super Nintendo controller. + * L2/R2/L3/R3 buttons correspond to the PS1 DualShock. + * Also used as id values for RETRO_DEVICE_INDEX_ANALOG_BUTTON */ +#define RETRO_DEVICE_ID_JOYPAD_B 0 +#define RETRO_DEVICE_ID_JOYPAD_Y 1 +#define RETRO_DEVICE_ID_JOYPAD_SELECT 2 +#define RETRO_DEVICE_ID_JOYPAD_START 3 +#define RETRO_DEVICE_ID_JOYPAD_UP 4 +#define RETRO_DEVICE_ID_JOYPAD_DOWN 5 +#define RETRO_DEVICE_ID_JOYPAD_LEFT 6 +#define RETRO_DEVICE_ID_JOYPAD_RIGHT 7 +#define RETRO_DEVICE_ID_JOYPAD_A 8 +#define RETRO_DEVICE_ID_JOYPAD_X 9 +#define RETRO_DEVICE_ID_JOYPAD_L 10 +#define RETRO_DEVICE_ID_JOYPAD_R 11 +#define RETRO_DEVICE_ID_JOYPAD_L2 12 +#define RETRO_DEVICE_ID_JOYPAD_R2 13 +#define RETRO_DEVICE_ID_JOYPAD_L3 14 +#define RETRO_DEVICE_ID_JOYPAD_R3 15 + +#define RETRO_DEVICE_ID_JOYPAD_MASK 256 + +/* Index / Id values for ANALOG device. */ +#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0 +#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1 +#define RETRO_DEVICE_INDEX_ANALOG_BUTTON 2 +#define RETRO_DEVICE_ID_ANALOG_X 0 +#define RETRO_DEVICE_ID_ANALOG_Y 1 + +/* Id values for MOUSE. */ +#define RETRO_DEVICE_ID_MOUSE_X 0 +#define RETRO_DEVICE_ID_MOUSE_Y 1 +#define RETRO_DEVICE_ID_MOUSE_LEFT 2 +#define RETRO_DEVICE_ID_MOUSE_RIGHT 3 +#define RETRO_DEVICE_ID_MOUSE_WHEELUP 4 +#define RETRO_DEVICE_ID_MOUSE_WHEELDOWN 5 +#define RETRO_DEVICE_ID_MOUSE_MIDDLE 6 +#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7 +#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8 +#define RETRO_DEVICE_ID_MOUSE_BUTTON_4 9 +#define RETRO_DEVICE_ID_MOUSE_BUTTON_5 10 + +/* Id values for LIGHTGUN. */ +#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X 13 /*Absolute Position*/ +#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y 14 /*Absolute*/ +#define RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN 15 /*Status Check*/ +#define RETRO_DEVICE_ID_LIGHTGUN_TRIGGER 2 +#define RETRO_DEVICE_ID_LIGHTGUN_RELOAD 16 /*Forced off-screen shot*/ +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_A 3 +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_B 4 +#define RETRO_DEVICE_ID_LIGHTGUN_START 6 +#define RETRO_DEVICE_ID_LIGHTGUN_SELECT 7 +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_C 8 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_UP 9 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_DOWN 10 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_LEFT 11 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_RIGHT 12 +/* deprecated */ +#define RETRO_DEVICE_ID_LIGHTGUN_X 0 /*Relative Position*/ +#define RETRO_DEVICE_ID_LIGHTGUN_Y 1 /*Relative*/ +#define RETRO_DEVICE_ID_LIGHTGUN_CURSOR 3 /*Use Aux:A*/ +#define RETRO_DEVICE_ID_LIGHTGUN_TURBO 4 /*Use Aux:B*/ +#define RETRO_DEVICE_ID_LIGHTGUN_PAUSE 5 /*Use Start*/ + +/* Id values for POINTER. */ +#define RETRO_DEVICE_ID_POINTER_X 0 +#define RETRO_DEVICE_ID_POINTER_Y 1 +#define RETRO_DEVICE_ID_POINTER_PRESSED 2 +#define RETRO_DEVICE_ID_POINTER_COUNT 3 + +/* Returned from retro_get_region(). */ +#define RETRO_REGION_NTSC 0 +#define RETRO_REGION_PAL 1 + +/* Id values for LANGUAGE */ +enum retro_language +{ + RETRO_LANGUAGE_ENGLISH = 0, + RETRO_LANGUAGE_JAPANESE = 1, + RETRO_LANGUAGE_FRENCH = 2, + RETRO_LANGUAGE_SPANISH = 3, + RETRO_LANGUAGE_GERMAN = 4, + RETRO_LANGUAGE_ITALIAN = 5, + RETRO_LANGUAGE_DUTCH = 6, + RETRO_LANGUAGE_PORTUGUESE_BRAZIL = 7, + RETRO_LANGUAGE_PORTUGUESE_PORTUGAL = 8, + RETRO_LANGUAGE_RUSSIAN = 9, + RETRO_LANGUAGE_KOREAN = 10, + RETRO_LANGUAGE_CHINESE_TRADITIONAL = 11, + RETRO_LANGUAGE_CHINESE_SIMPLIFIED = 12, + RETRO_LANGUAGE_ESPERANTO = 13, + RETRO_LANGUAGE_POLISH = 14, + RETRO_LANGUAGE_VIETNAMESE = 15, + RETRO_LANGUAGE_ARABIC = 16, + RETRO_LANGUAGE_GREEK = 17, + RETRO_LANGUAGE_TURKISH = 18, + RETRO_LANGUAGE_LAST, + + /* Ensure sizeof(enum) == sizeof(int) */ + RETRO_LANGUAGE_DUMMY = INT_MAX +}; + +/* Passed to retro_get_memory_data/size(). + * If the memory type doesn't apply to the + * implementation NULL/0 can be returned. + */ +#define RETRO_MEMORY_MASK 0xff + +/* Regular save RAM. This RAM is usually found on a game cartridge, + * backed up by a battery. + * If save game data is too complex for a single memory buffer, + * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment + * callback can be used. */ +#define RETRO_MEMORY_SAVE_RAM 0 + +/* Some games have a built-in clock to keep track of time. + * This memory is usually just a couple of bytes to keep track of time. + */ +#define RETRO_MEMORY_RTC 1 + +/* System ram lets a frontend peek into a game systems main RAM. */ +#define RETRO_MEMORY_SYSTEM_RAM 2 + +/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */ +#define RETRO_MEMORY_VIDEO_RAM 3 + +/* Keysyms used for ID in input state callback when polling RETRO_KEYBOARD. */ +enum retro_key +{ + RETROK_UNKNOWN = 0, + RETROK_FIRST = 0, + RETROK_BACKSPACE = 8, + RETROK_TAB = 9, + RETROK_CLEAR = 12, + RETROK_RETURN = 13, + RETROK_PAUSE = 19, + RETROK_ESCAPE = 27, + RETROK_SPACE = 32, + RETROK_EXCLAIM = 33, + RETROK_QUOTEDBL = 34, + RETROK_HASH = 35, + RETROK_DOLLAR = 36, + RETROK_AMPERSAND = 38, + RETROK_QUOTE = 39, + RETROK_LEFTPAREN = 40, + RETROK_RIGHTPAREN = 41, + RETROK_ASTERISK = 42, + RETROK_PLUS = 43, + RETROK_COMMA = 44, + RETROK_MINUS = 45, + RETROK_PERIOD = 46, + RETROK_SLASH = 47, + RETROK_0 = 48, + RETROK_1 = 49, + RETROK_2 = 50, + RETROK_3 = 51, + RETROK_4 = 52, + RETROK_5 = 53, + RETROK_6 = 54, + RETROK_7 = 55, + RETROK_8 = 56, + RETROK_9 = 57, + RETROK_COLON = 58, + RETROK_SEMICOLON = 59, + RETROK_LESS = 60, + RETROK_EQUALS = 61, + RETROK_GREATER = 62, + RETROK_QUESTION = 63, + RETROK_AT = 64, + RETROK_LEFTBRACKET = 91, + RETROK_BACKSLASH = 92, + RETROK_RIGHTBRACKET = 93, + RETROK_CARET = 94, + RETROK_UNDERSCORE = 95, + RETROK_BACKQUOTE = 96, + RETROK_a = 97, + RETROK_b = 98, + RETROK_c = 99, + RETROK_d = 100, + RETROK_e = 101, + RETROK_f = 102, + RETROK_g = 103, + RETROK_h = 104, + RETROK_i = 105, + RETROK_j = 106, + RETROK_k = 107, + RETROK_l = 108, + RETROK_m = 109, + RETROK_n = 110, + RETROK_o = 111, + RETROK_p = 112, + RETROK_q = 113, + RETROK_r = 114, + RETROK_s = 115, + RETROK_t = 116, + RETROK_u = 117, + RETROK_v = 118, + RETROK_w = 119, + RETROK_x = 120, + RETROK_y = 121, + RETROK_z = 122, + RETROK_LEFTBRACE = 123, + RETROK_BAR = 124, + RETROK_RIGHTBRACE = 125, + RETROK_TILDE = 126, + RETROK_DELETE = 127, + + RETROK_KP0 = 256, + RETROK_KP1 = 257, + RETROK_KP2 = 258, + RETROK_KP3 = 259, + RETROK_KP4 = 260, + RETROK_KP5 = 261, + RETROK_KP6 = 262, + RETROK_KP7 = 263, + RETROK_KP8 = 264, + RETROK_KP9 = 265, + RETROK_KP_PERIOD = 266, + RETROK_KP_DIVIDE = 267, + RETROK_KP_MULTIPLY = 268, + RETROK_KP_MINUS = 269, + RETROK_KP_PLUS = 270, + RETROK_KP_ENTER = 271, + RETROK_KP_EQUALS = 272, + + RETROK_UP = 273, + RETROK_DOWN = 274, + RETROK_RIGHT = 275, + RETROK_LEFT = 276, + RETROK_INSERT = 277, + RETROK_HOME = 278, + RETROK_END = 279, + RETROK_PAGEUP = 280, + RETROK_PAGEDOWN = 281, + + RETROK_F1 = 282, + RETROK_F2 = 283, + RETROK_F3 = 284, + RETROK_F4 = 285, + RETROK_F5 = 286, + RETROK_F6 = 287, + RETROK_F7 = 288, + RETROK_F8 = 289, + RETROK_F9 = 290, + RETROK_F10 = 291, + RETROK_F11 = 292, + RETROK_F12 = 293, + RETROK_F13 = 294, + RETROK_F14 = 295, + RETROK_F15 = 296, + + RETROK_NUMLOCK = 300, + RETROK_CAPSLOCK = 301, + RETROK_SCROLLOCK = 302, + RETROK_RSHIFT = 303, + RETROK_LSHIFT = 304, + RETROK_RCTRL = 305, + RETROK_LCTRL = 306, + RETROK_RALT = 307, + RETROK_LALT = 308, + RETROK_RMETA = 309, + RETROK_LMETA = 310, + RETROK_LSUPER = 311, + RETROK_RSUPER = 312, + RETROK_MODE = 313, + RETROK_COMPOSE = 314, + + RETROK_HELP = 315, + RETROK_PRINT = 316, + RETROK_SYSREQ = 317, + RETROK_BREAK = 318, + RETROK_MENU = 319, + RETROK_POWER = 320, + RETROK_EURO = 321, + RETROK_UNDO = 322, + RETROK_OEM_102 = 323, + + RETROK_LAST, + + RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */ +}; + +enum retro_mod +{ + RETROKMOD_NONE = 0x0000, + + RETROKMOD_SHIFT = 0x01, + RETROKMOD_CTRL = 0x02, + RETROKMOD_ALT = 0x04, + RETROKMOD_META = 0x08, + + RETROKMOD_NUMLOCK = 0x10, + RETROKMOD_CAPSLOCK = 0x20, + RETROKMOD_SCROLLOCK = 0x40, + + RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */ +}; + +/* If set, this call is not part of the public libretro API yet. It can + * change or be removed at any time. */ +#define RETRO_ENVIRONMENT_EXPERIMENTAL 0x10000 +/* Environment callback to be used internally in frontend. */ +#define RETRO_ENVIRONMENT_PRIVATE 0x20000 + +/* Environment commands. */ +#define RETRO_ENVIRONMENT_SET_ROTATION 1 /* const unsigned * -- + * Sets screen rotation of graphics. + * Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180, + * 270 degrees counter-clockwise respectively. + */ +#define RETRO_ENVIRONMENT_GET_OVERSCAN 2 /* bool * -- + * NOTE: As of 2019 this callback is considered deprecated in favor of + * using core options to manage overscan in a more nuanced, core-specific way. + * + * Boolean value whether or not the implementation should use overscan, + * or crop away overscan. + */ +#define RETRO_ENVIRONMENT_GET_CAN_DUPE 3 /* bool * -- + * Boolean value whether or not frontend supports frame duping, + * passing NULL to video frame callback. + */ + + /* Environ 4, 5 are no longer supported (GET_VARIABLE / SET_VARIABLES), + * and reserved to avoid possible ABI clash. + */ + +#define RETRO_ENVIRONMENT_SET_MESSAGE 6 /* const struct retro_message * -- + * Sets a message to be displayed in implementation-specific manner + * for a certain amount of 'frames'. + * Should not be used for trivial messages, which should simply be + * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a + * fallback, stderr). + */ +#define RETRO_ENVIRONMENT_SHUTDOWN 7 /* N/A (NULL) -- + * Requests the frontend to shutdown. + * Should only be used if game has a specific + * way to shutdown the game from a menu item or similar. + */ +#define RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL 8 + /* const unsigned * -- + * Gives a hint to the frontend how demanding this implementation + * is on a system. E.g. reporting a level of 2 means + * this implementation should run decently on all frontends + * of level 2 and up. + * + * It can be used by the frontend to potentially warn + * about too demanding implementations. + * + * The levels are "floating". + * + * This function can be called on a per-game basis, + * as certain games an implementation can play might be + * particularly demanding. + * If called, it should be called in retro_load_game(). + */ +#define RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY 9 + /* const char ** -- + * Returns the "system" directory of the frontend. + * This directory can be used to store system specific + * content such as BIOSes, configuration data, etc. + * The returned value can be NULL. + * If so, no such directory is defined, + * and it's up to the implementation to find a suitable directory. + * + * NOTE: Some cores used this folder also for "save" data such as + * memory cards, etc, for lack of a better place to put it. + * This is now discouraged, and if possible, cores should try to + * use the new GET_SAVE_DIRECTORY. + */ +#define RETRO_ENVIRONMENT_SET_PIXEL_FORMAT 10 + /* const enum retro_pixel_format * -- + * Sets the internal pixel format used by the implementation. + * The default pixel format is RETRO_PIXEL_FORMAT_0RGB1555. + * This pixel format however, is deprecated (see enum retro_pixel_format). + * If the call returns false, the frontend does not support this pixel + * format. + * + * This function should be called inside retro_load_game() or + * retro_get_system_av_info(). + */ +#define RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS 11 + /* const struct retro_input_descriptor * -- + * Sets an array of retro_input_descriptors. + * It is up to the frontend to present this in a usable way. + * The array is terminated by retro_input_descriptor::description + * being set to NULL. + * This function can be called at any time, but it is recommended + * to call it as early as possible. + */ +#define RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK 12 + /* const struct retro_keyboard_callback * -- + * Sets a callback function used to notify core about keyboard events. + */ +#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE 13 + /* const struct retro_disk_control_callback * -- + * Sets an interface which frontend can use to eject and insert + * disk images. + * This is used for games which consist of multiple images and + * must be manually swapped out by the user (e.g. PSX). + */ +#define RETRO_ENVIRONMENT_SET_HW_RENDER 14 + /* struct retro_hw_render_callback * -- + * Sets an interface to let a libretro core render with + * hardware acceleration. + * Should be called in retro_load_game(). + * If successful, libretro cores will be able to render to a + * frontend-provided framebuffer. + * The size of this framebuffer will be at least as large as + * max_width/max_height provided in get_av_info(). + * If HW rendering is used, pass only RETRO_HW_FRAME_BUFFER_VALID or + * NULL to retro_video_refresh_t. + */ +#define RETRO_ENVIRONMENT_GET_VARIABLE 15 + /* struct retro_variable * -- + * Interface to acquire user-defined information from environment + * that cannot feasibly be supported in a multi-system way. + * 'key' should be set to a key which has already been set by + * SET_VARIABLES. + * 'data' will be set to a value or NULL. + */ +#define RETRO_ENVIRONMENT_SET_VARIABLES 16 + /* const struct retro_variable * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterward it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * 'data' points to an array of retro_variable structs + * terminated by a { NULL, NULL } element. + * retro_variable::key should be namespaced to not collide + * with other implementations' keys. E.g. A core called + * 'foo' should use keys named as 'foo_option'. + * retro_variable::value should contain a human readable + * description of the key as well as a '|' delimited list + * of expected values. + * + * The number of possible options should be very limited, + * i.e. it should be feasible to cycle through options + * without a keyboard. + * + * First entry should be treated as a default. + * + * Example entry: + * { "foo_option", "Speed hack coprocessor X; false|true" } + * + * Text before first ';' is description. This ';' must be + * followed by a space, and followed by a list of possible + * values split up with '|'. + * + * Only strings are operated on. The possible values will + * generally be displayed and stored as-is by the frontend. + */ +#define RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE 17 + /* bool * -- + * Result is set to true if some variables are updated by + * frontend since last call to RETRO_ENVIRONMENT_GET_VARIABLE. + * Variables should be queried with GET_VARIABLE. + */ +#define RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME 18 + /* const bool * -- + * If true, the libretro implementation supports calls to + * retro_load_game() with NULL as argument. + * Used by cores which can run without particular game data. + * This should be called within retro_set_environment() only. + */ +#define RETRO_ENVIRONMENT_GET_LIBRETRO_PATH 19 + /* const char ** -- + * Retrieves the absolute path from where this libretro + * implementation was loaded. + * NULL is returned if the libretro was loaded statically + * (i.e. linked statically to frontend), or if the path cannot be + * determined. + * Mostly useful in cooperation with SET_SUPPORT_NO_GAME as assets can + * be loaded without ugly hacks. + */ + + /* Environment 20 was an obsolete version of SET_AUDIO_CALLBACK. + * It was not used by any known core at the time, + * and was removed from the API. */ +#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21 + /* const struct retro_frame_time_callback * -- + * Lets the core know how much time has passed since last + * invocation of retro_run(). + * The frontend can tamper with the timing to fake fast-forward, + * slow-motion, frame stepping, etc. + * In this case the delta time will use the reference value + * in frame_time_callback.. + */ +#define RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK 22 + /* const struct retro_audio_callback * -- + * Sets an interface which is used to notify a libretro core about audio + * being available for writing. + * The callback can be called from any thread, so a core using this must + * have a thread safe audio implementation. + * It is intended for games where audio and video are completely + * asynchronous and audio can be generated on the fly. + * This interface is not recommended for use with emulators which have + * highly synchronous audio. + * + * The callback only notifies about writability; the libretro core still + * has to call the normal audio callbacks + * to write audio. The audio callbacks must be called from within the + * notification callback. + * The amount of audio data to write is up to the implementation. + * Generally, the audio callback will be called continously in a loop. + * + * Due to thread safety guarantees and lack of sync between audio and + * video, a frontend can selectively disallow this interface based on + * internal configuration. A core using this interface must also + * implement the "normal" audio interface. + * + * A libretro core using SET_AUDIO_CALLBACK should also make use of + * SET_FRAME_TIME_CALLBACK. + */ +#define RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE 23 + /* struct retro_rumble_interface * -- + * Gets an interface which is used by a libretro core to set + * state of rumble motors in controllers. + * A strong and weak motor is supported, and they can be + * controlled indepedently. + */ +#define RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES 24 + /* uint64_t * -- + * Gets a bitmask telling which device type are expected to be + * handled properly in a call to retro_input_state_t. + * Devices which are not handled or recognized always return + * 0 in retro_input_state_t. + * Example bitmask: caps = (1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG). + * Should only be called in retro_run(). + */ +#define RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE (25 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_sensor_interface * -- + * Gets access to the sensor interface. + * The purpose of this interface is to allow + * setting state related to sensors such as polling rate, + * enabling/disable it entirely, etc. + * Reading sensor state is done via the normal + * input_state_callback API. + */ +#define RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE (26 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_camera_callback * -- + * Gets an interface to a video camera driver. + * A libretro core can use this interface to get access to a + * video camera. + * New video frames are delivered in a callback in same + * thread as retro_run(). + * + * GET_CAMERA_INTERFACE should be called in retro_load_game(). + * + * Depending on the camera implementation used, camera frames + * will be delivered as a raw framebuffer, + * or as an OpenGL texture directly. + * + * The core has to tell the frontend here which types of + * buffers can be handled properly. + * An OpenGL texture can only be handled when using a + * libretro GL core (SET_HW_RENDER). + * It is recommended to use a libretro GL core when + * using camera interface. + * + * The camera is not started automatically. The retrieved start/stop + * functions must be used to explicitly + * start and stop the camera driver. + */ +#define RETRO_ENVIRONMENT_GET_LOG_INTERFACE 27 + /* struct retro_log_callback * -- + * Gets an interface for logging. This is useful for + * logging in a cross-platform way + * as certain platforms cannot use stderr for logging. + * It also allows the frontend to + * show logging information in a more suitable way. + * If this interface is not used, libretro cores should + * log to stderr as desired. + */ +#define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28 + /* struct retro_perf_callback * -- + * Gets an interface for performance counters. This is useful + * for performance logging in a cross-platform way and for detecting + * architecture-specific features, such as SIMD support. + */ +#define RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE 29 + /* struct retro_location_callback * -- + * Gets access to the location interface. + * The purpose of this interface is to be able to retrieve + * location-based information from the host device, + * such as current latitude / longitude. + */ +#define RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY 30 /* Old name, kept for compatibility. */ +#define RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY 30 + /* const char ** -- + * Returns the "core assets" directory of the frontend. + * This directory can be used to store specific assets that the + * core relies upon, such as art assets, + * input data, etc etc. + * The returned value can be NULL. + * If so, no such directory is defined, + * and it's up to the implementation to find a suitable directory. + */ +#define RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY 31 + /* const char ** -- + * Returns the "save" directory of the frontend, unless there is no + * save directory available. The save directory should be used to + * store SRAM, memory cards, high scores, etc, if the libretro core + * cannot use the regular memory interface (retro_get_memory_data()). + * + * If the frontend cannot designate a save directory, it will return + * NULL to indicate that the core should attempt to operate without a + * save directory set. + * + * NOTE: early libretro cores used the system directory for save + * files. Cores that need to be backwards-compatible can still check + * GET_SYSTEM_DIRECTORY. + */ +#define RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO 32 + /* const struct retro_system_av_info * -- + * Sets a new av_info structure. This can only be called from + * within retro_run(). + * This should *only* be used if the core is completely altering the + * internal resolutions, aspect ratios, timings, sampling rate, etc. + * Calling this can require a full reinitialization of video/audio + * drivers in the frontend, + * + * so it is important to call it very sparingly, and usually only with + * the users explicit consent. + * An eventual driver reinitialize will happen so that video and + * audio callbacks + * happening after this call within the same retro_run() call will + * target the newly initialized driver. + * + * This callback makes it possible to support configurable resolutions + * in games, which can be useful to + * avoid setting the "worst case" in max_width/max_height. + * + * ***HIGHLY RECOMMENDED*** Do not call this callback every time + * resolution changes in an emulator core if it's + * expected to be a temporary change, for the reasons of possible + * driver reinitialization. + * This call is not a free pass for not trying to provide + * correct values in retro_get_system_av_info(). If you need to change + * things like aspect ratio or nominal width/height, + * use RETRO_ENVIRONMENT_SET_GEOMETRY, which is a softer variant + * of SET_SYSTEM_AV_INFO. + * + * If this returns false, the frontend does not acknowledge a + * changed av_info struct. + */ +#define RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK 33 + /* const struct retro_get_proc_address_interface * -- + * Allows a libretro core to announce support for the + * get_proc_address() interface. + * This interface allows for a standard way to extend libretro where + * use of environment calls are too indirect, + * e.g. for cases where the frontend wants to call directly into the core. + * + * If a core wants to expose this interface, SET_PROC_ADDRESS_CALLBACK + * **MUST** be called from within retro_set_environment(). + */ +#define RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO 34 + /* const struct retro_subsystem_info * -- + * This environment call introduces the concept of libretro "subsystems". + * A subsystem is a variant of a libretro core which supports + * different kinds of games. + * The purpose of this is to support e.g. emulators which might + * have special needs, e.g. Super Nintendo's Super GameBoy, Sufami Turbo. + * It can also be used to pick among subsystems in an explicit way + * if the libretro implementation is a multi-system emulator itself. + * + * Loading a game via a subsystem is done with retro_load_game_special(), + * and this environment call allows a libretro core to expose which + * subsystems are supported for use with retro_load_game_special(). + * A core passes an array of retro_game_special_info which is terminated + * with a zeroed out retro_game_special_info struct. + * + * If a core wants to use this functionality, SET_SUBSYSTEM_INFO + * **MUST** be called from within retro_set_environment(). + */ +#define RETRO_ENVIRONMENT_SET_CONTROLLER_INFO 35 + /* const struct retro_controller_info * -- + * This environment call lets a libretro core tell the frontend + * which controller subclasses are recognized in calls to + * retro_set_controller_port_device(). + * + * Some emulators such as Super Nintendo support multiple lightgun + * types which must be specifically selected from. It is therefore + * sometimes necessary for a frontend to be able to tell the core + * about a special kind of input device which is not specifcally + * provided by the Libretro API. + * + * In order for a frontend to understand the workings of those devices, + * they must be defined as a specialized subclass of the generic device + * types already defined in the libretro API. + * + * The core must pass an array of const struct retro_controller_info which + * is terminated with a blanked out struct. Each element of the + * retro_controller_info struct corresponds to the ascending port index + * that is passed to retro_set_controller_port_device() when that function + * is called to indicate to the core that the frontend has changed the + * active device subclass. SEE ALSO: retro_set_controller_port_device() + * + * The ascending input port indexes provided by the core in the struct + * are generally presented by frontends as ascending User # or Player #, + * such as Player 1, Player 2, Player 3, etc. Which device subclasses are + * supported can vary per input port. + * + * The first inner element of each entry in the retro_controller_info array + * is a retro_controller_description struct that specifies the names and + * codes of all device subclasses that are available for the corresponding + * User or Player, beginning with the generic Libretro device that the + * subclasses are derived from. The second inner element of each entry is the + * total number of subclasses that are listed in the retro_controller_description. + * + * NOTE: Even if special device types are set in the libretro core, + * libretro should only poll input based on the base input device types. + */ +#define RETRO_ENVIRONMENT_SET_MEMORY_MAPS (36 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const struct retro_memory_map * -- + * This environment call lets a libretro core tell the frontend + * about the memory maps this core emulates. + * This can be used to implement, for example, cheats in a core-agnostic way. + * + * Should only be used by emulators; it doesn't make much sense for + * anything else. + * It is recommended to expose all relevant pointers through + * retro_get_memory_* as well. + * + * Can be called from retro_init and retro_load_game. + */ +#define RETRO_ENVIRONMENT_SET_GEOMETRY 37 + /* const struct retro_game_geometry * -- + * This environment call is similar to SET_SYSTEM_AV_INFO for changing + * video parameters, but provides a guarantee that drivers will not be + * reinitialized. + * This can only be called from within retro_run(). + * + * The purpose of this call is to allow a core to alter nominal + * width/heights as well as aspect ratios on-the-fly, which can be + * useful for some emulators to change in run-time. + * + * max_width/max_height arguments are ignored and cannot be changed + * with this call as this could potentially require a reinitialization or a + * non-constant time operation. + * If max_width/max_height are to be changed, SET_SYSTEM_AV_INFO is required. + * + * A frontend must guarantee that this environment call completes in + * constant time. + */ +#define RETRO_ENVIRONMENT_GET_USERNAME 38 + /* const char ** + * Returns the specified username of the frontend, if specified by the user. + * This username can be used as a nickname for a core that has online facilities + * or any other mode where personalization of the user is desirable. + * The returned value can be NULL. + * If this environ callback is used by a core that requires a valid username, + * a default username should be specified by the core. + */ +#define RETRO_ENVIRONMENT_GET_LANGUAGE 39 + /* unsigned * -- + * Returns the specified language of the frontend, if specified by the user. + * It can be used by the core for localization purposes. + */ +#define RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER (40 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_framebuffer * -- + * Returns a preallocated framebuffer which the core can use for rendering + * the frame into when not using SET_HW_RENDER. + * The framebuffer returned from this call must not be used + * after the current call to retro_run() returns. + * + * The goal of this call is to allow zero-copy behavior where a core + * can render directly into video memory, avoiding extra bandwidth cost by copying + * memory from core to video memory. + * + * If this call succeeds and the core renders into it, + * the framebuffer pointer and pitch can be passed to retro_video_refresh_t. + * If the buffer from GET_CURRENT_SOFTWARE_FRAMEBUFFER is to be used, + * the core must pass the exact + * same pointer as returned by GET_CURRENT_SOFTWARE_FRAMEBUFFER; + * i.e. passing a pointer which is offset from the + * buffer is undefined. The width, height and pitch parameters + * must also match exactly to the values obtained from GET_CURRENT_SOFTWARE_FRAMEBUFFER. + * + * It is possible for a frontend to return a different pixel format + * than the one used in SET_PIXEL_FORMAT. This can happen if the frontend + * needs to perform conversion. + * + * It is still valid for a core to render to a different buffer + * even if GET_CURRENT_SOFTWARE_FRAMEBUFFER succeeds. + * + * A frontend must make sure that the pointer obtained from this function is + * writeable (and readable). + */ +#define RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE (41 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const struct retro_hw_render_interface ** -- + * Returns an API specific rendering interface for accessing API specific data. + * Not all HW rendering APIs support or need this. + * The contents of the returned pointer is specific to the rendering API + * being used. See the various headers like libretro_vulkan.h, etc. + * + * GET_HW_RENDER_INTERFACE cannot be called before context_reset has been called. + * Similarly, after context_destroyed callback returns, + * the contents of the HW_RENDER_INTERFACE are invalidated. + */ +#define RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS (42 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const bool * -- + * If true, the libretro implementation supports achievements + * either via memory descriptors set with RETRO_ENVIRONMENT_SET_MEMORY_MAPS + * or via retro_get_memory_data/retro_get_memory_size. + * + * This must be called before the first call to retro_run. + */ +#define RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE (43 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const struct retro_hw_render_context_negotiation_interface * -- + * Sets an interface which lets the libretro core negotiate with frontend how a context is created. + * The semantics of this interface depends on which API is used in SET_HW_RENDER earlier. + * This interface will be used when the frontend is trying to create a HW rendering context, + * so it will be used after SET_HW_RENDER, but before the context_reset callback. + */ +#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 44 + /* uint64_t * -- + * Sets quirk flags associated with serialization. The frontend will zero any flags it doesn't + * recognize or support. Should be set in either retro_init or retro_load_game, but not both. + */ +#define RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT (44 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* N/A (null) * -- + * The frontend will try to use a 'shared' hardware context (mostly applicable + * to OpenGL) when a hardware context is being set up. + * + * Returns true if the frontend supports shared hardware contexts and false + * if the frontend does not support shared hardware contexts. + * + * This will do nothing on its own until SET_HW_RENDER env callbacks are + * being used. + */ +#define RETRO_ENVIRONMENT_GET_VFS_INTERFACE (45 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_vfs_interface_info * -- + * Gets access to the VFS interface. + * VFS presence needs to be queried prior to load_game or any + * get_system/save/other_directory being called to let front end know + * core supports VFS before it starts handing out paths. + * It is recomended to do so in retro_set_environment + */ +#define RETRO_ENVIRONMENT_GET_LED_INTERFACE (46 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_led_interface * -- + * Gets an interface which is used by a libretro core to set + * state of LEDs. + */ +#define RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE (47 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* int * -- + * Tells the core if the frontend wants audio or video. + * If disabled, the frontend will discard the audio or video, + * so the core may decide to skip generating a frame or generating audio. + * This is mainly used for increasing performance. + * Bit 0 (value 1): Enable Video + * Bit 1 (value 2): Enable Audio + * Bit 2 (value 4): Use Fast Savestates. + * Bit 3 (value 8): Hard Disable Audio + * Other bits are reserved for future use and will default to zero. + * If video is disabled: + * * The frontend wants the core to not generate any video, + * including presenting frames via hardware acceleration. + * * The frontend's video frame callback will do nothing. + * * After running the frame, the video output of the next frame should be + * no different than if video was enabled, and saving and loading state + * should have no issues. + * If audio is disabled: + * * The frontend wants the core to not generate any audio. + * * The frontend's audio callbacks will do nothing. + * * After running the frame, the audio output of the next frame should be + * no different than if audio was enabled, and saving and loading state + * should have no issues. + * Fast Savestates: + * * Guaranteed to be created by the same binary that will load them. + * * Will not be written to or read from the disk. + * * Suggest that the core assumes loading state will succeed. + * * Suggest that the core updates its memory buffers in-place if possible. + * * Suggest that the core skips clearing memory. + * * Suggest that the core skips resetting the system. + * * Suggest that the core may skip validation steps. + * Hard Disable Audio: + * * Used for a secondary core when running ahead. + * * Indicates that the frontend will never need audio from the core. + * * Suggests that the core may stop synthesizing audio, but this should not + * compromise emulation accuracy. + * * Audio output for the next frame does not matter, and the frontend will + * never need an accurate audio state in the future. + * * State will never be saved when using Hard Disable Audio. + */ +#define RETRO_ENVIRONMENT_GET_MIDI_INTERFACE (48 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_midi_interface ** -- + * Returns a MIDI interface that can be used for raw data I/O. + */ + +#define RETRO_ENVIRONMENT_GET_FASTFORWARDING (49 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* bool * -- + * Boolean value that indicates whether or not the frontend is in + * fastforwarding mode. + */ + +#define RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE (50 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* float * -- + * Float value that lets us know what target refresh rate + * is curently in use by the frontend. + * + * The core can use the returned value to set an ideal + * refresh rate/framerate. + */ + +#define RETRO_ENVIRONMENT_GET_INPUT_BITMASKS (51 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* bool * -- + * Boolean value that indicates whether or not the frontend supports + * input bitmasks being returned by retro_input_state_t. The advantage + * of this is that retro_input_state_t has to be only called once to + * grab all button states instead of multiple times. + * + * If it returns true, you can pass RETRO_DEVICE_ID_JOYPAD_MASK as 'id' + * to retro_input_state_t (make sure 'device' is set to RETRO_DEVICE_JOYPAD). + * It will return a bitmask of all the digital buttons. + */ + +#define RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION 52 + /* unsigned * -- + * Unsigned value is the API version number of the core options + * interface supported by the frontend. If callback return false, + * API version is assumed to be 0. + * + * In legacy code, core options are set by passing an array of + * retro_variable structs to RETRO_ENVIRONMENT_SET_VARIABLES. + * This may be still be done regardless of the core options + * interface version. + * + * If version is 1 however, core options may instead be set by + * passing an array of retro_core_option_definition structs to + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS, or a 2D array of + * retro_core_option_definition structs to RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL. + * This allows the core to additionally set option sublabel information + * and/or provide localisation support. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS 53 + /* const struct retro_core_option_definition ** -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_ENHANCED_CORE_OPTIONS + * returns an API version of 1. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * 'data' points to an array of retro_core_option_definition structs + * terminated by a { NULL, NULL, NULL, {{0}}, NULL } element. + * retro_core_option_definition::key should be namespaced to not collide + * with other implementations' keys. e.g. A core called + * 'foo' should use keys named as 'foo_option'. + * retro_core_option_definition::desc should contain a human readable + * description of the key. + * retro_core_option_definition::info should contain any additional human + * readable information text that a typical user may need to + * understand the functionality of the option. + * retro_core_option_definition::values is an array of retro_core_option_value + * structs terminated by a { NULL, NULL } element. + * > retro_core_option_definition::values[index].value is an expected option + * value. + * > retro_core_option_definition::values[index].label is a human readable + * label used when displaying the value on screen. If NULL, + * the value itself is used. + * retro_core_option_definition::default_value is the default core option + * setting. It must match one of the expected option values in the + * retro_core_option_definition::values array. If it does not, or the + * default value is NULL, the first entry in the + * retro_core_option_definition::values array is treated as the default. + * + * The number of possible options should be very limited, + * and must be less than RETRO_NUM_CORE_OPTION_VALUES_MAX. + * i.e. it should be feasible to cycle through options + * without a keyboard. + * + * First entry should be treated as a default. + * + * Example entry: + * { + * "foo_option", + * "Speed hack coprocessor X", + * "Provides increased performance at the expense of reduced accuracy", + * { + * { "false", NULL }, + * { "true", NULL }, + * { "unstable", "Turbo (Unstable)" }, + * { NULL, NULL }, + * }, + * "false" + * } + * + * Only strings are operated on. The possible values will + * generally be displayed and stored as-is by the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL 54 + /* const struct retro_core_options_intl * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_ENHANCED_CORE_OPTIONS + * returns an API version of 1. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * This is fundamentally the same as RETRO_ENVIRONMENT_SET_CORE_OPTIONS, + * with the addition of localisation support. The description of the + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS callback should be consulted + * for further details. + * + * 'data' points to a retro_core_options_intl struct. + * + * retro_core_options_intl::us is a pointer to an array of + * retro_core_option_definition structs defining the US English + * core options implementation. It must point to a valid array. + * + * retro_core_options_intl::local is a pointer to an array of + * retro_core_option_definition structs defining core options for + * the current frontend language. It may be NULL (in which case + * retro_core_options_intl::us is used by the frontend). Any items + * missing from this array will be read from retro_core_options_intl::us + * instead. + * + * NOTE: Default core option values are always taken from the + * retro_core_options_intl::us array. Any default values in + * retro_core_options_intl::local array will be ignored. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY 55 + /* struct retro_core_option_display * -- + * + * Allows an implementation to signal the environment to show + * or hide a variable when displaying core options. This is + * considered a *suggestion*. The frontend is free to ignore + * this callback, and its implementation not considered mandatory. + * + * 'data' points to a retro_core_option_display struct + * + * retro_core_option_display::key is a variable identifier + * which has already been set by SET_VARIABLES/SET_CORE_OPTIONS. + * + * retro_core_option_display::visible is a boolean, specifying + * whether variable should be displayed + * + * Note that all core option variables will be set visible by + * default when calling SET_VARIABLES/SET_CORE_OPTIONS. + */ + +/* VFS functionality */ + +/* File paths: + * File paths passed as parameters when using this API shall be well formed UNIX-style, + * using "/" (unquoted forward slash) as directory separator regardless of the platform's native separator. + * Paths shall also include at least one forward slash ("game.bin" is an invalid path, use "./game.bin" instead). + * Other than the directory separator, cores shall not make assumptions about path format: + * "C:/path/game.bin", "http://example.com/game.bin", "#game/game.bin", "./game.bin" (without quotes) are all valid paths. + * Cores may replace the basename or remove path components from the end, and/or add new components; + * however, cores shall not append "./", "../" or multiple consecutive forward slashes ("//") to paths they request to front end. + * The frontend is encouraged to make such paths work as well as it can, but is allowed to give up if the core alters paths too much. + * Frontends are encouraged, but not required, to support native file system paths (modulo replacing the directory separator, if applicable). + * Cores are allowed to try using them, but must remain functional if the front rejects such requests. + * Cores are encouraged to use the libretro-common filestream functions for file I/O, + * as they seamlessly integrate with VFS, deal with directory separator replacement as appropriate + * and provide platform-specific fallbacks in cases where front ends do not support VFS. */ + +/* Opaque file handle + * Introduced in VFS API v1 */ +struct retro_vfs_file_handle; + +/* Opaque directory handle + * Introduced in VFS API v3 */ +struct retro_vfs_dir_handle; + +/* File open flags + * Introduced in VFS API v1 */ +#define RETRO_VFS_FILE_ACCESS_READ (1 << 0) /* Read only mode */ +#define RETRO_VFS_FILE_ACCESS_WRITE (1 << 1) /* Write only mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified */ +#define RETRO_VFS_FILE_ACCESS_READ_WRITE (RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE) /* Read-write mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified*/ +#define RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING (1 << 2) /* Prevents discarding content of existing files opened for writing */ + +/* These are only hints. The frontend may choose to ignore them. Other than RAM/CPU/etc use, + and how they react to unlikely external interference (for example someone else writing to that file, + or the file's server going down), behavior will not change. */ +#define RETRO_VFS_FILE_ACCESS_HINT_NONE (0) +/* Indicate that the file will be accessed many times. The frontend should aggressively cache everything. */ +#define RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS (1 << 0) + +/* Seek positions */ +#define RETRO_VFS_SEEK_POSITION_START 0 +#define RETRO_VFS_SEEK_POSITION_CURRENT 1 +#define RETRO_VFS_SEEK_POSITION_END 2 + +/* stat() result flags + * Introduced in VFS API v3 */ +#define RETRO_VFS_STAT_IS_VALID (1 << 0) +#define RETRO_VFS_STAT_IS_DIRECTORY (1 << 1) +#define RETRO_VFS_STAT_IS_CHARACTER_SPECIAL (1 << 2) + +/* Get path from opaque handle. Returns the exact same path passed to file_open when getting the handle + * Introduced in VFS API v1 */ +typedef const char *(RETRO_CALLCONV *retro_vfs_get_path_t)(struct retro_vfs_file_handle *stream); + +/* Open a file for reading or writing. If path points to a directory, this will + * fail. Returns the opaque file handle, or NULL for error. + * Introduced in VFS API v1 */ +typedef struct retro_vfs_file_handle *(RETRO_CALLCONV *retro_vfs_open_t)(const char *path, unsigned mode, unsigned hints); + +/* Close the file and release its resources. Must be called if open_file returns non-NULL. Returns 0 on success, -1 on failure. + * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used. + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_close_t)(struct retro_vfs_file_handle *stream); + +/* Return the size of the file in bytes, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_size_t)(struct retro_vfs_file_handle *stream); + +/* Truncate file to specified size. Returns 0 on success or -1 on error + * Introduced in VFS API v2 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_truncate_t)(struct retro_vfs_file_handle *stream, int64_t length); + +/* Get the current read / write position for the file. Returns -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_tell_t)(struct retro_vfs_file_handle *stream); + +/* Set the current read/write position for the file. Returns the new position, -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_seek_t)(struct retro_vfs_file_handle *stream, int64_t offset, int seek_position); + +/* Read data from a file. Returns the number of bytes read, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_read_t)(struct retro_vfs_file_handle *stream, void *s, uint64_t len); + +/* Write data to a file. Returns the number of bytes written, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_write_t)(struct retro_vfs_file_handle *stream, const void *s, uint64_t len); + +/* Flush pending writes to file, if using buffered IO. Returns 0 on sucess, or -1 on failure. + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_flush_t)(struct retro_vfs_file_handle *stream); + +/* Delete the specified file. Returns 0 on success, -1 on failure + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_remove_t)(const char *path); + +/* Rename the specified file. Returns 0 on success, -1 on failure + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_rename_t)(const char *old_path, const char *new_path); + +/* Stat the specified file. Retruns a bitmask of RETRO_VFS_STAT_* flags, none are set if path was not valid. + * Additionally stores file size in given variable, unless NULL is given. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size); + +/* Create the specified directory. Returns 0 on success, -1 on unknown failure, -2 if already exists. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_mkdir_t)(const char *dir); + +/* Open the specified directory for listing. Returns the opaque dir handle, or NULL for error. + * Support for the include_hidden argument may vary depending on the platform. + * Introduced in VFS API v3 */ +typedef struct retro_vfs_dir_handle *(RETRO_CALLCONV *retro_vfs_opendir_t)(const char *dir, bool include_hidden); + +/* Read the directory entry at the current position, and move the read pointer to the next position. + * Returns true on success, false if already on the last entry. + * Introduced in VFS API v3 */ +typedef bool (RETRO_CALLCONV *retro_vfs_readdir_t)(struct retro_vfs_dir_handle *dirstream); + +/* Get the name of the last entry read. Returns a string on success, or NULL for error. + * The returned string pointer is valid until the next call to readdir or closedir. + * Introduced in VFS API v3 */ +typedef const char *(RETRO_CALLCONV *retro_vfs_dirent_get_name_t)(struct retro_vfs_dir_handle *dirstream); + +/* Check if the last entry read was a directory. Returns true if it was, false otherwise (or on error). + * Introduced in VFS API v3 */ +typedef bool (RETRO_CALLCONV *retro_vfs_dirent_is_dir_t)(struct retro_vfs_dir_handle *dirstream); + +/* Close the directory and release its resources. Must be called if opendir returns non-NULL. Returns 0 on success, -1 on failure. + * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_closedir_t)(struct retro_vfs_dir_handle *dirstream); + +struct retro_vfs_interface +{ + /* VFS API v1 */ + retro_vfs_get_path_t get_path; + retro_vfs_open_t open; + retro_vfs_close_t close; + retro_vfs_size_t size; + retro_vfs_tell_t tell; + retro_vfs_seek_t seek; + retro_vfs_read_t read; + retro_vfs_write_t write; + retro_vfs_flush_t flush; + retro_vfs_remove_t remove; + retro_vfs_rename_t rename; + /* VFS API v2 */ + retro_vfs_truncate_t truncate; + /* VFS API v3 */ + retro_vfs_stat_t stat; + retro_vfs_mkdir_t mkdir; + retro_vfs_opendir_t opendir; + retro_vfs_readdir_t readdir; + retro_vfs_dirent_get_name_t dirent_get_name; + retro_vfs_dirent_is_dir_t dirent_is_dir; + retro_vfs_closedir_t closedir; +}; + +struct retro_vfs_interface_info +{ + /* Set by core: should this be higher than the version the front end supports, + * front end will return false in the RETRO_ENVIRONMENT_GET_VFS_INTERFACE call + * Introduced in VFS API v1 */ + uint32_t required_interface_version; + + /* Frontend writes interface pointer here. The frontend also sets the actual + * version, must be at least required_interface_version. + * Introduced in VFS API v1 */ + struct retro_vfs_interface *iface; +}; + +enum retro_hw_render_interface_type +{ + RETRO_HW_RENDER_INTERFACE_VULKAN = 0, + RETRO_HW_RENDER_INTERFACE_D3D9 = 1, + RETRO_HW_RENDER_INTERFACE_D3D10 = 2, + RETRO_HW_RENDER_INTERFACE_D3D11 = 3, + RETRO_HW_RENDER_INTERFACE_D3D12 = 4, + RETRO_HW_RENDER_INTERFACE_GSKIT_PS2 = 5, + RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX +}; + +/* Base struct. All retro_hw_render_interface_* types + * contain at least these fields. */ +struct retro_hw_render_interface +{ + enum retro_hw_render_interface_type interface_type; + unsigned interface_version; +}; + +typedef void (RETRO_CALLCONV *retro_set_led_state_t)(int led, int state); +struct retro_led_interface +{ + retro_set_led_state_t set_led_state; +}; + +/* Retrieves the current state of the MIDI input. + * Returns true if it's enabled, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_input_enabled_t)(void); + +/* Retrieves the current state of the MIDI output. + * Returns true if it's enabled, false otherwise */ +typedef bool (RETRO_CALLCONV *retro_midi_output_enabled_t)(void); + +/* Reads next byte from the input stream. + * Returns true if byte is read, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_read_t)(uint8_t *byte); + +/* Writes byte to the output stream. + * 'delta_time' is in microseconds and represent time elapsed since previous write. + * Returns true if byte is written, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_write_t)(uint8_t byte, uint32_t delta_time); + +/* Flushes previously written data. + * Returns true if successful, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_flush_t)(void); + +struct retro_midi_interface +{ + retro_midi_input_enabled_t input_enabled; + retro_midi_output_enabled_t output_enabled; + retro_midi_read_t read; + retro_midi_write_t write; + retro_midi_flush_t flush; +}; + +enum retro_hw_render_context_negotiation_interface_type +{ + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN = 0, + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_DUMMY = INT_MAX +}; + +/* Base struct. All retro_hw_render_context_negotiation_interface_* types + * contain at least these fields. */ +struct retro_hw_render_context_negotiation_interface +{ + enum retro_hw_render_context_negotiation_interface_type interface_type; + unsigned interface_version; +}; + +/* Serialized state is incomplete in some way. Set if serialization is + * usable in typical end-user cases but should not be relied upon to + * implement frame-sensitive frontend features such as netplay or + * rerecording. */ +#define RETRO_SERIALIZATION_QUIRK_INCOMPLETE (1 << 0) +/* The core must spend some time initializing before serialization is + * supported. retro_serialize() will initially fail; retro_unserialize() + * and retro_serialize_size() may or may not work correctly either. */ +#define RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE (1 << 1) +/* Serialization size may change within a session. */ +#define RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE (1 << 2) +/* Set by the frontend to acknowledge that it supports variable-sized + * states. */ +#define RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE (1 << 3) +/* Serialized state can only be loaded during the same session. */ +#define RETRO_SERIALIZATION_QUIRK_SINGLE_SESSION (1 << 4) +/* Serialized state cannot be loaded on an architecture with a different + * endianness from the one it was saved on. */ +#define RETRO_SERIALIZATION_QUIRK_ENDIAN_DEPENDENT (1 << 5) +/* Serialized state cannot be loaded on a different platform from the one it + * was saved on for reasons other than endianness, such as word size + * dependence */ +#define RETRO_SERIALIZATION_QUIRK_PLATFORM_DEPENDENT (1 << 6) + +#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */ +#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */ +#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2) /* The memory area is system RAM. This is main RAM of the gaming system. */ +#define RETRO_MEMDESC_SAVE_RAM (1 << 3) /* The memory area is save RAM. This RAM is usually found on a game cartridge, backed up by a battery. */ +#define RETRO_MEMDESC_VIDEO_RAM (1 << 4) /* The memory area is video RAM (VRAM) */ +#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */ +#define RETRO_MEMDESC_ALIGN_4 (2 << 16) +#define RETRO_MEMDESC_ALIGN_8 (3 << 16) +#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */ +#define RETRO_MEMDESC_MINSIZE_4 (2 << 24) +#define RETRO_MEMDESC_MINSIZE_8 (3 << 24) +struct retro_memory_descriptor +{ + uint64_t flags; + + /* Pointer to the start of the relevant ROM or RAM chip. + * It's strongly recommended to use 'offset' if possible, rather than + * doing math on the pointer. + * + * If the same byte is mapped my multiple descriptors, their descriptors + * must have the same pointer. + * If 'start' does not point to the first byte in the pointer, put the + * difference in 'offset' instead. + * + * May be NULL if there's nothing usable here (e.g. hardware registers and + * open bus). No flags should be set if the pointer is NULL. + * It's recommended to minimize the number of descriptors if possible, + * but not mandatory. */ + void *ptr; + size_t offset; + + /* This is the location in the emulated address space + * where the mapping starts. */ + size_t start; + + /* Which bits must be same as in 'start' for this mapping to apply. + * The first memory descriptor to claim a certain byte is the one + * that applies. + * A bit which is set in 'start' must also be set in this. + * Can be zero, in which case each byte is assumed mapped exactly once. + * In this case, 'len' must be a power of two. */ + size_t select; + + /* If this is nonzero, the set bits are assumed not connected to the + * memory chip's address pins. */ + size_t disconnect; + + /* This one tells the size of the current memory area. + * If, after start+disconnect are applied, the address is higher than + * this, the highest bit of the address is cleared. + * + * If the address is still too high, the next highest bit is cleared. + * Can be zero, in which case it's assumed to be infinite (as limited + * by 'select' and 'disconnect'). */ + size_t len; + + /* To go from emulated address to physical address, the following + * order applies: + * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */ + + /* The address space name must consist of only a-zA-Z0-9_-, + * should be as short as feasible (maximum length is 8 plus the NUL), + * and may not be any other address space plus one or more 0-9A-F + * at the end. + * However, multiple memory descriptors for the same address space is + * allowed, and the address space name can be empty. NULL is treated + * as empty. + * + * Address space names are case sensitive, but avoid lowercase if possible. + * The same pointer may exist in multiple address spaces. + * + * Examples: + * blank+blank - valid (multiple things may be mapped in the same namespace) + * 'Sp'+'Sp' - valid (multiple things may be mapped in the same namespace) + * 'A'+'B' - valid (neither is a prefix of each other) + * 'S'+blank - valid ('S' is not in 0-9A-F) + * 'a'+blank - valid ('a' is not in 0-9A-F) + * 'a'+'A' - valid (neither is a prefix of each other) + * 'AR'+blank - valid ('R' is not in 0-9A-F) + * 'ARB'+blank - valid (the B can't be part of the address either, because + * there is no namespace 'AR') + * blank+'B' - not valid, because it's ambigous which address space B1234 + * would refer to. + * The length can't be used for that purpose; the frontend may want + * to append arbitrary data to an address, without a separator. */ + const char *addrspace; + + /* TODO: When finalizing this one, add a description field, which should be + * "WRAM" or something roughly equally long. */ + + /* TODO: When finalizing this one, replace 'select' with 'limit', which tells + * which bits can vary and still refer to the same address (limit = ~select). + * TODO: limit? range? vary? something else? */ + + /* TODO: When finalizing this one, if 'len' is above what 'select' (or + * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len' + * and 'select' != 0, and the mappings don't tell how the system switches the + * banks. */ + + /* TODO: When finalizing this one, fix the 'len' bit removal order. + * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00. + * Algorithm: Take bits highest to lowest, but if it goes above len, clear + * the most recent addition and continue on the next bit. + * TODO: Can the above be optimized? Is "remove the lowest bit set in both + * pointer and 'len'" equivalent? */ + + /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing + * the emulated memory in 32-bit chunks, native endian. But that's nothing + * compared to Darek Mihocka + * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE + * RAM backwards! I'll want to represent both of those, via some flags. + * + * I suspect MAME either didn't think of that idea, or don't want the #ifdef. + * Not sure which, nor do I really care. */ + + /* TODO: Some of those flags are unused and/or don't really make sense. Clean + * them up. */ +}; + +/* The frontend may use the largest value of 'start'+'select' in a + * certain namespace to infer the size of the address space. + * + * If the address space is larger than that, a mapping with .ptr=NULL + * should be at the end of the array, with .select set to all ones for + * as long as the address space is big. + * + * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags): + * SNES WRAM: + * .start=0x7E0000, .len=0x20000 + * (Note that this must be mapped before the ROM in most cases; some of the + * ROM mappers + * try to claim $7E0000, or at least $7E8000.) + * SNES SPC700 RAM: + * .addrspace="S", .len=0x10000 + * SNES WRAM mirrors: + * .flags=MIRROR, .start=0x000000, .select=0xC0E000, .len=0x2000 + * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000 + * SNES WRAM mirrors, alternate equivalent descriptor: + * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF + * (Various similar constructions can be created by combining parts of + * the above two.) + * SNES LoROM (512KB, mirrored a couple of times): + * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024 + * .flags=CONST, .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024 + * SNES HiROM (4MB): + * .flags=CONST, .start=0x400000, .select=0x400000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x008000, .select=0x408000, .len=4*1024*1024 + * SNES ExHiROM (8MB): + * .flags=CONST, .offset=0, .start=0xC00000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024, .start=0x400000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x808000, .select=0xC08000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024+0x8000, .start=0x008000, .select=0xC08000, .len=4*1024*1024 + * Clarify the size of the address space: + * .ptr=NULL, .select=0xFFFFFF + * .len can be implied by .select in many of them, but was included for clarity. + */ + +struct retro_memory_map +{ + const struct retro_memory_descriptor *descriptors; + unsigned num_descriptors; +}; + +struct retro_controller_description +{ + /* Human-readable description of the controller. Even if using a generic + * input device type, this can be set to the particular device type the + * core uses. */ + const char *desc; + + /* Device type passed to retro_set_controller_port_device(). If the device + * type is a sub-class of a generic input device type, use the + * RETRO_DEVICE_SUBCLASS macro to create an ID. + * + * E.g. RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 1). */ + unsigned id; +}; + +struct retro_controller_info +{ + const struct retro_controller_description *types; + unsigned num_types; +}; + +struct retro_subsystem_memory_info +{ + /* The extension associated with a memory type, e.g. "psram". */ + const char *extension; + + /* The memory type for retro_get_memory(). This should be at + * least 0x100 to avoid conflict with standardized + * libretro memory types. */ + unsigned type; +}; + +struct retro_subsystem_rom_info +{ + /* Describes what the content is (SGB BIOS, GB ROM, etc). */ + const char *desc; + + /* Same definition as retro_get_system_info(). */ + const char *valid_extensions; + + /* Same definition as retro_get_system_info(). */ + bool need_fullpath; + + /* Same definition as retro_get_system_info(). */ + bool block_extract; + + /* This is set if the content is required to load a game. + * If this is set to false, a zeroed-out retro_game_info can be passed. */ + bool required; + + /* Content can have multiple associated persistent + * memory types (retro_get_memory()). */ + const struct retro_subsystem_memory_info *memory; + unsigned num_memory; +}; + +struct retro_subsystem_info +{ + /* Human-readable string of the subsystem type, e.g. "Super GameBoy" */ + const char *desc; + + /* A computer friendly short string identifier for the subsystem type. + * This name must be [a-z]. + * E.g. if desc is "Super GameBoy", this can be "sgb". + * This identifier can be used for command-line interfaces, etc. + */ + const char *ident; + + /* Infos for each content file. The first entry is assumed to be the + * "most significant" content for frontend purposes. + * E.g. with Super GameBoy, the first content should be the GameBoy ROM, + * as it is the most "significant" content to a user. + * If a frontend creates new file paths based on the content used + * (e.g. savestates), it should use the path for the first ROM to do so. */ + const struct retro_subsystem_rom_info *roms; + + /* Number of content files associated with a subsystem. */ + unsigned num_roms; + + /* The type passed to retro_load_game_special(). */ + unsigned id; +}; + +typedef void (RETRO_CALLCONV *retro_proc_address_t)(void); + +/* libretro API extension functions: + * (None here so far). + * + * Get a symbol from a libretro core. + * Cores should only return symbols which are actual + * extensions to the libretro API. + * + * Frontends should not use this to obtain symbols to standard + * libretro entry points (static linking or dlsym). + * + * The symbol name must be equal to the function name, + * e.g. if void retro_foo(void); exists, the symbol must be called "retro_foo". + * The returned function pointer must be cast to the corresponding type. + */ +typedef retro_proc_address_t (RETRO_CALLCONV *retro_get_proc_address_t)(const char *sym); + +struct retro_get_proc_address_interface +{ + retro_get_proc_address_t get_proc_address; +}; + +enum retro_log_level +{ + RETRO_LOG_DEBUG = 0, + RETRO_LOG_INFO, + RETRO_LOG_WARN, + RETRO_LOG_ERROR, + + RETRO_LOG_DUMMY = INT_MAX +}; + +/* Logging function. Takes log level argument as well. */ +typedef void (RETRO_CALLCONV *retro_log_printf_t)(enum retro_log_level level, + const char *fmt, ...); + +struct retro_log_callback +{ + retro_log_printf_t log; +}; + +/* Performance related functions */ + +/* ID values for SIMD CPU features */ +#define RETRO_SIMD_SSE (1 << 0) +#define RETRO_SIMD_SSE2 (1 << 1) +#define RETRO_SIMD_VMX (1 << 2) +#define RETRO_SIMD_VMX128 (1 << 3) +#define RETRO_SIMD_AVX (1 << 4) +#define RETRO_SIMD_NEON (1 << 5) +#define RETRO_SIMD_SSE3 (1 << 6) +#define RETRO_SIMD_SSSE3 (1 << 7) +#define RETRO_SIMD_MMX (1 << 8) +#define RETRO_SIMD_MMXEXT (1 << 9) +#define RETRO_SIMD_SSE4 (1 << 10) +#define RETRO_SIMD_SSE42 (1 << 11) +#define RETRO_SIMD_AVX2 (1 << 12) +#define RETRO_SIMD_VFPU (1 << 13) +#define RETRO_SIMD_PS (1 << 14) +#define RETRO_SIMD_AES (1 << 15) +#define RETRO_SIMD_VFPV3 (1 << 16) +#define RETRO_SIMD_VFPV4 (1 << 17) +#define RETRO_SIMD_POPCNT (1 << 18) +#define RETRO_SIMD_MOVBE (1 << 19) +#define RETRO_SIMD_CMOV (1 << 20) +#define RETRO_SIMD_ASIMD (1 << 21) + +typedef uint64_t retro_perf_tick_t; +typedef int64_t retro_time_t; + +struct retro_perf_counter +{ + const char *ident; + retro_perf_tick_t start; + retro_perf_tick_t total; + retro_perf_tick_t call_cnt; + + bool registered; +}; + +/* Returns current time in microseconds. + * Tries to use the most accurate timer available. + */ +typedef retro_time_t (RETRO_CALLCONV *retro_perf_get_time_usec_t)(void); + +/* A simple counter. Usually nanoseconds, but can also be CPU cycles. + * Can be used directly if desired (when creating a more sophisticated + * performance counter system). + * */ +typedef retro_perf_tick_t (RETRO_CALLCONV *retro_perf_get_counter_t)(void); + +/* Returns a bit-mask of detected CPU features (RETRO_SIMD_*). */ +typedef uint64_t (RETRO_CALLCONV *retro_get_cpu_features_t)(void); + +/* Asks frontend to log and/or display the state of performance counters. + * Performance counters can always be poked into manually as well. + */ +typedef void (RETRO_CALLCONV *retro_perf_log_t)(void); + +/* Register a performance counter. + * ident field must be set with a discrete value and other values in + * retro_perf_counter must be 0. + * Registering can be called multiple times. To avoid calling to + * frontend redundantly, you can check registered field first. */ +typedef void (RETRO_CALLCONV *retro_perf_register_t)(struct retro_perf_counter *counter); + +/* Starts a registered counter. */ +typedef void (RETRO_CALLCONV *retro_perf_start_t)(struct retro_perf_counter *counter); + +/* Stops a registered counter. */ +typedef void (RETRO_CALLCONV *retro_perf_stop_t)(struct retro_perf_counter *counter); + +/* For convenience it can be useful to wrap register, start and stop in macros. + * E.g.: + * #ifdef LOG_PERFORMANCE + * #define RETRO_PERFORMANCE_INIT(perf_cb, name) static struct retro_perf_counter name = {#name}; if (!name.registered) perf_cb.perf_register(&(name)) + * #define RETRO_PERFORMANCE_START(perf_cb, name) perf_cb.perf_start(&(name)) + * #define RETRO_PERFORMANCE_STOP(perf_cb, name) perf_cb.perf_stop(&(name)) + * #else + * ... Blank macros ... + * #endif + * + * These can then be used mid-functions around code snippets. + * + * extern struct retro_perf_callback perf_cb; * Somewhere in the core. + * + * void do_some_heavy_work(void) + * { + * RETRO_PERFORMANCE_INIT(cb, work_1; + * RETRO_PERFORMANCE_START(cb, work_1); + * heavy_work_1(); + * RETRO_PERFORMANCE_STOP(cb, work_1); + * + * RETRO_PERFORMANCE_INIT(cb, work_2); + * RETRO_PERFORMANCE_START(cb, work_2); + * heavy_work_2(); + * RETRO_PERFORMANCE_STOP(cb, work_2); + * } + * + * void retro_deinit(void) + * { + * perf_cb.perf_log(); * Log all perf counters here for example. + * } + */ + +struct retro_perf_callback +{ + retro_perf_get_time_usec_t get_time_usec; + retro_get_cpu_features_t get_cpu_features; + + retro_perf_get_counter_t get_perf_counter; + retro_perf_register_t perf_register; + retro_perf_start_t perf_start; + retro_perf_stop_t perf_stop; + retro_perf_log_t perf_log; +}; + +/* FIXME: Document the sensor API and work out behavior. + * It will be marked as experimental until then. + */ +enum retro_sensor_action +{ + RETRO_SENSOR_ACCELEROMETER_ENABLE = 0, + RETRO_SENSOR_ACCELEROMETER_DISABLE, + + RETRO_SENSOR_DUMMY = INT_MAX +}; + +/* Id values for SENSOR types. */ +#define RETRO_SENSOR_ACCELEROMETER_X 0 +#define RETRO_SENSOR_ACCELEROMETER_Y 1 +#define RETRO_SENSOR_ACCELEROMETER_Z 2 + +typedef bool (RETRO_CALLCONV *retro_set_sensor_state_t)(unsigned port, + enum retro_sensor_action action, unsigned rate); + +typedef float (RETRO_CALLCONV *retro_sensor_get_input_t)(unsigned port, unsigned id); + +struct retro_sensor_interface +{ + retro_set_sensor_state_t set_sensor_state; + retro_sensor_get_input_t get_sensor_input; +}; + +enum retro_camera_buffer +{ + RETRO_CAMERA_BUFFER_OPENGL_TEXTURE = 0, + RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER, + + RETRO_CAMERA_BUFFER_DUMMY = INT_MAX +}; + +/* Starts the camera driver. Can only be called in retro_run(). */ +typedef bool (RETRO_CALLCONV *retro_camera_start_t)(void); + +/* Stops the camera driver. Can only be called in retro_run(). */ +typedef void (RETRO_CALLCONV *retro_camera_stop_t)(void); + +/* Callback which signals when the camera driver is initialized + * and/or deinitialized. + * retro_camera_start_t can be called in initialized callback. + */ +typedef void (RETRO_CALLCONV *retro_camera_lifetime_status_t)(void); + +/* A callback for raw framebuffer data. buffer points to an XRGB8888 buffer. + * Width, height and pitch are similar to retro_video_refresh_t. + * First pixel is top-left origin. + */ +typedef void (RETRO_CALLCONV *retro_camera_frame_raw_framebuffer_t)(const uint32_t *buffer, + unsigned width, unsigned height, size_t pitch); + +/* A callback for when OpenGL textures are used. + * + * texture_id is a texture owned by camera driver. + * Its state or content should be considered immutable, except for things like + * texture filtering and clamping. + * + * texture_target is the texture target for the GL texture. + * These can include e.g. GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE, and possibly + * more depending on extensions. + * + * affine points to a packed 3x3 column-major matrix used to apply an affine + * transform to texture coordinates. (affine_matrix * vec3(coord_x, coord_y, 1.0)) + * After transform, normalized texture coord (0, 0) should be bottom-left + * and (1, 1) should be top-right (or (width, height) for RECTANGLE). + * + * GL-specific typedefs are avoided here to avoid relying on gl.h in + * the API definition. + */ +typedef void (RETRO_CALLCONV *retro_camera_frame_opengl_texture_t)(unsigned texture_id, + unsigned texture_target, const float *affine); + +struct retro_camera_callback +{ + /* Set by libretro core. + * Example bitmask: caps = (1 << RETRO_CAMERA_BUFFER_OPENGL_TEXTURE) | (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER). + */ + uint64_t caps; + + /* Desired resolution for camera. Is only used as a hint. */ + unsigned width; + unsigned height; + + /* Set by frontend. */ + retro_camera_start_t start; + retro_camera_stop_t stop; + + /* Set by libretro core if raw framebuffer callbacks will be used. */ + retro_camera_frame_raw_framebuffer_t frame_raw_framebuffer; + + /* Set by libretro core if OpenGL texture callbacks will be used. */ + retro_camera_frame_opengl_texture_t frame_opengl_texture; + + /* Set by libretro core. Called after camera driver is initialized and + * ready to be started. + * Can be NULL, in which this callback is not called. + */ + retro_camera_lifetime_status_t initialized; + + /* Set by libretro core. Called right before camera driver is + * deinitialized. + * Can be NULL, in which this callback is not called. + */ + retro_camera_lifetime_status_t deinitialized; +}; + +/* Sets the interval of time and/or distance at which to update/poll + * location-based data. + * + * To ensure compatibility with all location-based implementations, + * values for both interval_ms and interval_distance should be provided. + * + * interval_ms is the interval expressed in milliseconds. + * interval_distance is the distance interval expressed in meters. + */ +typedef void (RETRO_CALLCONV *retro_location_set_interval_t)(unsigned interval_ms, + unsigned interval_distance); + +/* Start location services. The device will start listening for changes to the + * current location at regular intervals (which are defined with + * retro_location_set_interval_t). */ +typedef bool (RETRO_CALLCONV *retro_location_start_t)(void); + +/* Stop location services. The device will stop listening for changes + * to the current location. */ +typedef void (RETRO_CALLCONV *retro_location_stop_t)(void); + +/* Get the position of the current location. Will set parameters to + * 0 if no new location update has happened since the last time. */ +typedef bool (RETRO_CALLCONV *retro_location_get_position_t)(double *lat, double *lon, + double *horiz_accuracy, double *vert_accuracy); + +/* Callback which signals when the location driver is initialized + * and/or deinitialized. + * retro_location_start_t can be called in initialized callback. + */ +typedef void (RETRO_CALLCONV *retro_location_lifetime_status_t)(void); + +struct retro_location_callback +{ + retro_location_start_t start; + retro_location_stop_t stop; + retro_location_get_position_t get_position; + retro_location_set_interval_t set_interval; + + retro_location_lifetime_status_t initialized; + retro_location_lifetime_status_t deinitialized; +}; + +enum retro_rumble_effect +{ + RETRO_RUMBLE_STRONG = 0, + RETRO_RUMBLE_WEAK = 1, + + RETRO_RUMBLE_DUMMY = INT_MAX +}; + +/* Sets rumble state for joypad plugged in port 'port'. + * Rumble effects are controlled independently, + * and setting e.g. strong rumble does not override weak rumble. + * Strength has a range of [0, 0xffff]. + * + * Returns true if rumble state request was honored. + * Calling this before first retro_run() is likely to return false. */ +typedef bool (RETRO_CALLCONV *retro_set_rumble_state_t)(unsigned port, + enum retro_rumble_effect effect, uint16_t strength); + +struct retro_rumble_interface +{ + retro_set_rumble_state_t set_rumble_state; +}; + +/* Notifies libretro that audio data should be written. */ +typedef void (RETRO_CALLCONV *retro_audio_callback_t)(void); + +/* True: Audio driver in frontend is active, and callback is + * expected to be called regularily. + * False: Audio driver in frontend is paused or inactive. + * Audio callback will not be called until set_state has been + * called with true. + * Initial state is false (inactive). + */ +typedef void (RETRO_CALLCONV *retro_audio_set_state_callback_t)(bool enabled); + +struct retro_audio_callback +{ + retro_audio_callback_t callback; + retro_audio_set_state_callback_t set_state; +}; + +/* Notifies a libretro core of time spent since last invocation + * of retro_run() in microseconds. + * + * It will be called right before retro_run() every frame. + * The frontend can tamper with timing to support cases like + * fast-forward, slow-motion and framestepping. + * + * In those scenarios the reference frame time value will be used. */ +typedef int64_t retro_usec_t; +typedef void (RETRO_CALLCONV *retro_frame_time_callback_t)(retro_usec_t usec); +struct retro_frame_time_callback +{ + retro_frame_time_callback_t callback; + /* Represents the time of one frame. It is computed as + * 1000000 / fps, but the implementation will resolve the + * rounding to ensure that framestepping, etc is exact. */ + retro_usec_t reference; +}; + +/* Pass this to retro_video_refresh_t if rendering to hardware. + * Passing NULL to retro_video_refresh_t is still a frame dupe as normal. + * */ +#define RETRO_HW_FRAME_BUFFER_VALID ((void*)-1) + +/* Invalidates the current HW context. + * Any GL state is lost, and must not be deinitialized explicitly. + * If explicit deinitialization is desired by the libretro core, + * it should implement context_destroy callback. + * If called, all GPU resources must be reinitialized. + * Usually called when frontend reinits video driver. + * Also called first time video driver is initialized, + * allowing libretro core to initialize resources. + */ +typedef void (RETRO_CALLCONV *retro_hw_context_reset_t)(void); + +/* Gets current framebuffer which is to be rendered to. + * Could change every frame potentially. + */ +typedef uintptr_t (RETRO_CALLCONV *retro_hw_get_current_framebuffer_t)(void); + +/* Get a symbol from HW context. */ +typedef retro_proc_address_t (RETRO_CALLCONV *retro_hw_get_proc_address_t)(const char *sym); + +enum retro_hw_context_type +{ + RETRO_HW_CONTEXT_NONE = 0, + /* OpenGL 2.x. Driver can choose to use latest compatibility context. */ + RETRO_HW_CONTEXT_OPENGL = 1, + /* OpenGL ES 2.0. */ + RETRO_HW_CONTEXT_OPENGLES2 = 2, + /* Modern desktop core GL context. Use version_major/ + * version_minor fields to set GL version. */ + RETRO_HW_CONTEXT_OPENGL_CORE = 3, + /* OpenGL ES 3.0 */ + RETRO_HW_CONTEXT_OPENGLES3 = 4, + /* OpenGL ES 3.1+. Set version_major/version_minor. For GLES2 and GLES3, + * use the corresponding enums directly. */ + RETRO_HW_CONTEXT_OPENGLES_VERSION = 5, + + /* Vulkan, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE. */ + RETRO_HW_CONTEXT_VULKAN = 6, + + /* Direct3D, set version_major to select the type of interface + * returned by RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */ + RETRO_HW_CONTEXT_DIRECT3D = 7, + + RETRO_HW_CONTEXT_DUMMY = INT_MAX +}; + +struct retro_hw_render_callback +{ + /* Which API to use. Set by libretro core. */ + enum retro_hw_context_type context_type; + + /* Called when a context has been created or when it has been reset. + * An OpenGL context is only valid after context_reset() has been called. + * + * When context_reset is called, OpenGL resources in the libretro + * implementation are guaranteed to be invalid. + * + * It is possible that context_reset is called multiple times during an + * application lifecycle. + * If context_reset is called without any notification (context_destroy), + * the OpenGL context was lost and resources should just be recreated + * without any attempt to "free" old resources. + */ + retro_hw_context_reset_t context_reset; + + /* Set by frontend. + * TODO: This is rather obsolete. The frontend should not + * be providing preallocated framebuffers. */ + retro_hw_get_current_framebuffer_t get_current_framebuffer; + + /* Set by frontend. + * Can return all relevant functions, including glClear on Windows. */ + retro_hw_get_proc_address_t get_proc_address; + + /* Set if render buffers should have depth component attached. + * TODO: Obsolete. */ + bool depth; + + /* Set if stencil buffers should be attached. + * TODO: Obsolete. */ + bool stencil; + + /* If depth and stencil are true, a packed 24/8 buffer will be added. + * Only attaching stencil is invalid and will be ignored. */ + + /* Use conventional bottom-left origin convention. If false, + * standard libretro top-left origin semantics are used. + * TODO: Move to GL specific interface. */ + bool bottom_left_origin; + + /* Major version number for core GL context or GLES 3.1+. */ + unsigned version_major; + + /* Minor version number for core GL context or GLES 3.1+. */ + unsigned version_minor; + + /* If this is true, the frontend will go very far to avoid + * resetting context in scenarios like toggling fullscreen, etc. + * TODO: Obsolete? Maybe frontend should just always assume this ... + */ + bool cache_context; + + /* The reset callback might still be called in extreme situations + * such as if the context is lost beyond recovery. + * + * For optimal stability, set this to false, and allow context to be + * reset at any time. + */ + + /* A callback to be called before the context is destroyed in a + * controlled way by the frontend. */ + retro_hw_context_reset_t context_destroy; + + /* OpenGL resources can be deinitialized cleanly at this step. + * context_destroy can be set to NULL, in which resources will + * just be destroyed without any notification. + * + * Even when context_destroy is non-NULL, it is possible that + * context_reset is called without any destroy notification. + * This happens if context is lost by external factors (such as + * notified by GL_ARB_robustness). + * + * In this case, the context is assumed to be already dead, + * and the libretro implementation must not try to free any OpenGL + * resources in the subsequent context_reset. + */ + + /* Creates a debug context. */ + bool debug_context; +}; + +/* Callback type passed in RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK. + * Called by the frontend in response to keyboard events. + * down is set if the key is being pressed, or false if it is being released. + * keycode is the RETROK value of the char. + * character is the text character of the pressed key. (UTF-32). + * key_modifiers is a set of RETROKMOD values or'ed together. + * + * The pressed/keycode state can be indepedent of the character. + * It is also possible that multiple characters are generated from a + * single keypress. + * Keycode events should be treated separately from character events. + * However, when possible, the frontend should try to synchronize these. + * If only a character is posted, keycode should be RETROK_UNKNOWN. + * + * Similarily if only a keycode event is generated with no corresponding + * character, character should be 0. + */ +typedef void (RETRO_CALLCONV *retro_keyboard_event_t)(bool down, unsigned keycode, + uint32_t character, uint16_t key_modifiers); + +struct retro_keyboard_callback +{ + retro_keyboard_event_t callback; +}; + +/* Callbacks for RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE. + * Should be set for implementations which can swap out multiple disk + * images in runtime. + * + * If the implementation can do this automatically, it should strive to do so. + * However, there are cases where the user must manually do so. + * + * Overview: To swap a disk image, eject the disk image with + * set_eject_state(true). + * Set the disk index with set_image_index(index). Insert the disk again + * with set_eject_state(false). + */ + +/* If ejected is true, "ejects" the virtual disk tray. + * When ejected, the disk image index can be set. + */ +typedef bool (RETRO_CALLCONV *retro_set_eject_state_t)(bool ejected); + +/* Gets current eject state. The initial state is 'not ejected'. */ +typedef bool (RETRO_CALLCONV *retro_get_eject_state_t)(void); + +/* Gets current disk index. First disk is index 0. + * If return value is >= get_num_images(), no disk is currently inserted. + */ +typedef unsigned (RETRO_CALLCONV *retro_get_image_index_t)(void); + +/* Sets image index. Can only be called when disk is ejected. + * The implementation supports setting "no disk" by using an + * index >= get_num_images(). + */ +typedef bool (RETRO_CALLCONV *retro_set_image_index_t)(unsigned index); + +/* Gets total number of images which are available to use. */ +typedef unsigned (RETRO_CALLCONV *retro_get_num_images_t)(void); + +struct retro_game_info; + +/* Replaces the disk image associated with index. + * Arguments to pass in info have same requirements as retro_load_game(). + * Virtual disk tray must be ejected when calling this. + * + * Replacing a disk image with info = NULL will remove the disk image + * from the internal list. + * As a result, calls to get_image_index() can change. + * + * E.g. replace_image_index(1, NULL), and previous get_image_index() + * returned 4 before. + * Index 1 will be removed, and the new index is 3. + */ +typedef bool (RETRO_CALLCONV *retro_replace_image_index_t)(unsigned index, + const struct retro_game_info *info); + +/* Adds a new valid index (get_num_images()) to the internal disk list. + * This will increment subsequent return values from get_num_images() by 1. + * This image index cannot be used until a disk image has been set + * with replace_image_index. */ +typedef bool (RETRO_CALLCONV *retro_add_image_index_t)(void); + +struct retro_disk_control_callback +{ + retro_set_eject_state_t set_eject_state; + retro_get_eject_state_t get_eject_state; + + retro_get_image_index_t get_image_index; + retro_set_image_index_t set_image_index; + retro_get_num_images_t get_num_images; + + retro_replace_image_index_t replace_image_index; + retro_add_image_index_t add_image_index; +}; + +enum retro_pixel_format +{ + /* 0RGB1555, native endian. + * 0 bit must be set to 0. + * This pixel format is default for compatibility concerns only. + * If a 15/16-bit pixel format is desired, consider using RGB565. */ + RETRO_PIXEL_FORMAT_0RGB1555 = 0, + + /* XRGB8888, native endian. + * X bits are ignored. */ + RETRO_PIXEL_FORMAT_XRGB8888 = 1, + + /* RGB565, native endian. + * This pixel format is the recommended format to use if a 15/16-bit + * format is desired as it is the pixel format that is typically + * available on a wide range of low-power devices. + * + * It is also natively supported in APIs like OpenGL ES. */ + RETRO_PIXEL_FORMAT_RGB565 = 2, + + /* Ensure sizeof() == sizeof(int). */ + RETRO_PIXEL_FORMAT_UNKNOWN = INT_MAX +}; + +struct retro_message +{ + const char *msg; /* Message to be displayed. */ + unsigned frames; /* Duration in frames of message. */ +}; + +/* Describes how the libretro implementation maps a libretro input bind + * to its internal input system through a human readable string. + * This string can be used to better let a user configure input. */ +struct retro_input_descriptor +{ + /* Associates given parameters with a description. */ + unsigned port; + unsigned device; + unsigned index; + unsigned id; + + /* Human readable description for parameters. + * The pointer must remain valid until + * retro_unload_game() is called. */ + const char *description; +}; + +struct retro_system_info +{ + /* All pointers are owned by libretro implementation, and pointers must + * remain valid until retro_deinit() is called. */ + + const char *library_name; /* Descriptive name of library. Should not + * contain any version numbers, etc. */ + const char *library_version; /* Descriptive version of core. */ + + const char *valid_extensions; /* A string listing probably content + * extensions the core will be able to + * load, separated with pipe. + * I.e. "bin|rom|iso". + * Typically used for a GUI to filter + * out extensions. */ + + /* Libretro cores that need to have direct access to their content + * files, including cores which use the path of the content files to + * determine the paths of other files, should set need_fullpath to true. + * + * Cores should strive for setting need_fullpath to false, + * as it allows the frontend to perform patching, etc. + * + * If need_fullpath is true and retro_load_game() is called: + * - retro_game_info::path is guaranteed to have a valid path + * - retro_game_info::data and retro_game_info::size are invalid + * + * If need_fullpath is false and retro_load_game() is called: + * - retro_game_info::path may be NULL + * - retro_game_info::data and retro_game_info::size are guaranteed + * to be valid + * + * See also: + * - RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY + * - RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY + */ + bool need_fullpath; + + /* If true, the frontend is not allowed to extract any archives before + * loading the real content. + * Necessary for certain libretro implementations that load games + * from zipped archives. */ + bool block_extract; +}; + +struct retro_game_geometry +{ + unsigned base_width; /* Nominal video width of game. */ + unsigned base_height; /* Nominal video height of game. */ + unsigned max_width; /* Maximum possible width of game. */ + unsigned max_height; /* Maximum possible height of game. */ + + float aspect_ratio; /* Nominal aspect ratio of game. If + * aspect_ratio is <= 0.0, an aspect ratio + * of base_width / base_height is assumed. + * A frontend could override this setting, + * if desired. */ +}; + +struct retro_system_timing +{ + double fps; /* FPS of video content. */ + double sample_rate; /* Sampling rate of audio. */ +}; + +struct retro_system_av_info +{ + struct retro_game_geometry geometry; + struct retro_system_timing timing; +}; + +struct retro_variable +{ + /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. + * If NULL, obtains the complete environment string if more + * complex parsing is necessary. + * The environment string is formatted as key-value pairs + * delimited by semicolons as so: + * "key1=value1;key2=value2;..." + */ + const char *key; + + /* Value to be obtained. If key does not exist, it is set to NULL. */ + const char *value; +}; + +struct retro_core_option_display +{ + /* Variable to configure in RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY */ + const char *key; + + /* Specifies whether variable should be displayed + * when presenting core options to the user */ + bool visible; +}; + +/* Maximum number of values permitted for a core option + * NOTE: This may be increased on a core-by-core basis + * if required (doing so has no effect on the frontend) */ +#define RETRO_NUM_CORE_OPTION_VALUES_MAX 128 + +struct retro_core_option_value +{ + /* Expected option value */ + const char *value; + + /* Human-readable value label. If NULL, value itself + * will be displayed by the frontend */ + const char *label; +}; + +struct retro_core_option_definition +{ + /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. */ + const char *key; + + /* Human-readable core option description (used as menu label) */ + const char *desc; + + /* Human-readable core option information (used as menu sublabel) */ + const char *info; + + /* Array of retro_core_option_value structs, terminated by NULL */ + struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX]; + + /* Default core option value. Must match one of the values + * in the retro_core_option_value array, otherwise will be + * ignored */ + const char *default_value; +}; + +struct retro_core_options_intl +{ + /* Pointer to an array of retro_core_option_definition structs + * - US English implementation + * - Must point to a valid array */ + struct retro_core_option_definition *us; + + /* Pointer to an array of retro_core_option_definition structs + * - Implementation for current frontend language + * - May be NULL */ + struct retro_core_option_definition *local; +}; + +struct retro_game_info +{ + const char *path; /* Path to game, UTF-8 encoded. + * Sometimes used as a reference for building other paths. + * May be NULL if game was loaded from stdin or similar, + * but in this case some cores will be unable to load `data`. + * So, it is preferable to fabricate something here instead + * of passing NULL, which will help more cores to succeed. + * retro_system_info::need_fullpath requires + * that this path is valid. */ + const void *data; /* Memory buffer of loaded game. Will be NULL + * if need_fullpath was set. */ + size_t size; /* Size of memory buffer. */ + const char *meta; /* String of implementation specific meta-data. */ +}; + +#define RETRO_MEMORY_ACCESS_WRITE (1 << 0) + /* The core will write to the buffer provided by retro_framebuffer::data. */ +#define RETRO_MEMORY_ACCESS_READ (1 << 1) + /* The core will read from retro_framebuffer::data. */ +#define RETRO_MEMORY_TYPE_CACHED (1 << 0) + /* The memory in data is cached. + * If not cached, random writes and/or reading from the buffer is expected to be very slow. */ +struct retro_framebuffer +{ + void *data; /* The framebuffer which the core can render into. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. + The initial contents of data are unspecified. */ + unsigned width; /* The framebuffer width used by the core. Set by core. */ + unsigned height; /* The framebuffer height used by the core. Set by core. */ + size_t pitch; /* The number of bytes between the beginning of a scanline, + and beginning of the next scanline. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */ + enum retro_pixel_format format; /* The pixel format the core must use to render into data. + This format could differ from the format used in + SET_PIXEL_FORMAT. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */ + + unsigned access_flags; /* How the core will access the memory in the framebuffer. + RETRO_MEMORY_ACCESS_* flags. + Set by core. */ + unsigned memory_flags; /* Flags telling core how the memory has been mapped. + RETRO_MEMORY_TYPE_* flags. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */ +}; + +/* Callbacks */ + +/* Environment callback. Gives implementations a way of performing + * uncommon tasks. Extensible. */ +typedef bool (RETRO_CALLCONV *retro_environment_t)(unsigned cmd, void *data); + +/* Render a frame. Pixel format is 15-bit 0RGB1555 native endian + * unless changed (see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT). + * + * Width and height specify dimensions of buffer. + * Pitch specifices length in bytes between two lines in buffer. + * + * For performance reasons, it is highly recommended to have a frame + * that is packed in memory, i.e. pitch == width * byte_per_pixel. + * Certain graphic APIs, such as OpenGL ES, do not like textures + * that are not packed in memory. + */ +typedef void (RETRO_CALLCONV *retro_video_refresh_t)(const void *data, unsigned width, + unsigned height, size_t pitch); + +/* Renders a single audio frame. Should only be used if implementation + * generates a single sample at a time. + * Format is signed 16-bit native endian. + */ +typedef void (RETRO_CALLCONV *retro_audio_sample_t)(int16_t left, int16_t right); + +/* Renders multiple audio frames in one go. + * + * One frame is defined as a sample of left and right channels, interleaved. + * I.e. int16_t buf[4] = { l, r, l, r }; would be 2 frames. + * Only one of the audio callbacks must ever be used. + */ +typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_t)(const int16_t *data, + size_t frames); + +/* Polls input. */ +typedef void (RETRO_CALLCONV *retro_input_poll_t)(void); + +/* Queries for input for player 'port'. device will be masked with + * RETRO_DEVICE_MASK. + * + * Specialization of devices such as RETRO_DEVICE_JOYPAD_MULTITAP that + * have been set with retro_set_controller_port_device() + * will still use the higher level RETRO_DEVICE_JOYPAD to request input. + */ +typedef int16_t (RETRO_CALLCONV *retro_input_state_t)(unsigned port, unsigned device, + unsigned index, unsigned id); + +/* Sets callbacks. retro_set_environment() is guaranteed to be called + * before retro_init(). + * + * The rest of the set_* functions are guaranteed to have been called + * before the first call to retro_run() is made. */ +RETRO_API void retro_set_environment(retro_environment_t); +RETRO_API void retro_set_video_refresh(retro_video_refresh_t); +RETRO_API void retro_set_audio_sample(retro_audio_sample_t); +RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t); +RETRO_API void retro_set_input_poll(retro_input_poll_t); +RETRO_API void retro_set_input_state(retro_input_state_t); + +/* Library global initialization/deinitialization. */ +RETRO_API void retro_init(void); +RETRO_API void retro_deinit(void); + +/* Must return RETRO_API_VERSION. Used to validate ABI compatibility + * when the API is revised. */ +RETRO_API unsigned retro_api_version(void); + +/* Gets statically known system info. Pointers provided in *info + * must be statically allocated. + * Can be called at any time, even before retro_init(). */ +RETRO_API void retro_get_system_info(struct retro_system_info *info); + +/* Gets information about system audio/video timings and geometry. + * Can be called only after retro_load_game() has successfully completed. + * NOTE: The implementation of this function might not initialize every + * variable if needed. + * E.g. geom.aspect_ratio might not be initialized if core doesn't + * desire a particular aspect ratio. */ +RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info); + +/* Sets device to be used for player 'port'. + * By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all + * available ports. + * Setting a particular device type is not a guarantee that libretro cores + * will only poll input based on that particular device type. It is only a + * hint to the libretro core when a core cannot automatically detect the + * appropriate input device type on its own. It is also relevant when a + * core can change its behavior depending on device type. + * + * As part of the core's implementation of retro_set_controller_port_device, + * the core should call RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the + * frontend if the descriptions for any controls have changed as a + * result of changing the device type. + */ +RETRO_API void retro_set_controller_port_device(unsigned port, unsigned device); + +/* Resets the current game. */ +RETRO_API void retro_reset(void); + +/* Runs the game for one video frame. + * During retro_run(), input_poll callback must be called at least once. + * + * If a frame is not rendered for reasons where a game "dropped" a frame, + * this still counts as a frame, and retro_run() should explicitly dupe + * a frame if GET_CAN_DUPE returns true. + * In this case, the video callback can take a NULL argument for data. + */ +RETRO_API void retro_run(void); + +/* Returns the amount of data the implementation requires to serialize + * internal state (save states). + * Between calls to retro_load_game() and retro_unload_game(), the + * returned size is never allowed to be larger than a previous returned + * value, to ensure that the frontend can allocate a save state buffer once. + */ +RETRO_API size_t retro_serialize_size(void); + +/* Serializes internal state. If failed, or size is lower than + * retro_serialize_size(), it should return false, true otherwise. */ +RETRO_API bool retro_serialize(void *data, size_t size); +RETRO_API bool retro_unserialize(const void *data, size_t size); + +RETRO_API void retro_cheat_reset(void); +RETRO_API void retro_cheat_set(unsigned index, bool enabled, const char *code); + +/* Loads a game. + * Return true to indicate successful loading and false to indicate load failure. + */ +RETRO_API bool retro_load_game(const struct retro_game_info *game); + +/* Loads a "special" kind of game. Should not be used, + * except in extreme cases. */ +RETRO_API bool retro_load_game_special( + unsigned game_type, + const struct retro_game_info *info, size_t num_info +); + +/* Unloads the currently loaded game. Called before retro_deinit(void). */ +RETRO_API void retro_unload_game(void); + +/* Gets region of game. */ +RETRO_API unsigned retro_get_region(void); + +/* Gets region of memory. */ +RETRO_API void *retro_get_memory_data(unsigned id); +RETRO_API size_t retro_get_memory_size(unsigned id); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/bsnes/gb/libretro/link.T b/bsnes/gb/libretro/link.T new file mode 100644 index 00000000..b0c262db --- /dev/null +++ b/bsnes/gb/libretro/link.T @@ -0,0 +1,5 @@ +{ + global: retro_*; + local: *; +}; + diff --git a/bsnes/gb/version.mk b/bsnes/gb/version.mk new file mode 100644 index 00000000..35ae0ad6 --- /dev/null +++ b/bsnes/gb/version.mk @@ -0,0 +1 @@ +VERSION := 0.13.6