├── .deepsource.toml ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── NOTE.md ├── README.md ├── assets ├── alleyway.gif ├── dmg-acid2.jpg ├── donkeykong.gif ├── frogger.gif ├── galaga.gif ├── mario.gif ├── mario2.gif ├── mortalkombat.gif ├── pacman.gif ├── roadrash.gif ├── spaceinvaders.gif ├── tetris.gif └── zelda.gif ├── core ├── Cargo.toml └── src │ ├── alu.rs │ ├── callbacks.rs │ ├── cartridge.rs │ ├── cgb_dma.rs │ ├── colour │ ├── bg_map_attributes.rs │ ├── colour.rs │ ├── grey_shades.rs │ ├── mod.rs │ └── palette_ram.rs │ ├── config.rs │ ├── constants.rs │ ├── cpu.rs │ ├── gpu.rs │ ├── helpers.rs │ ├── interrupts.rs │ ├── joypad.rs │ ├── lcd.rs │ ├── lib.rs │ ├── memory │ ├── battery_backed_ram.rs │ ├── cgb_speed_switch.rs │ ├── mbcs │ │ ├── mbc1.rs │ │ ├── mbc2.rs │ │ ├── mbc3.rs │ │ ├── mbc5.rs │ │ ├── mod.rs │ │ └── none.rs │ ├── memory.rs │ ├── mod.rs │ ├── ram.rs │ ├── rom.rs │ └── vram.rs │ ├── registers.rs │ ├── serial_cable.rs │ └── sound │ ├── apu.rs │ ├── channel1.rs │ ├── channel2.rs │ ├── channel3.rs │ ├── channel4.rs │ ├── length_function.rs │ ├── mod.rs │ ├── registers.rs │ └── volume_envelope.rs ├── libretro ├── Cargo.toml ├── libgbrs_libretro.info ├── run.sh └── src │ └── lib.rs ├── profiling ├── Cargo.toml └── src │ └── main.rs ├── roms ├── DMG-ACID2-LICENSE └── dmg-acid2.gb ├── sdl-gui ├── Cargo.toml ├── build.rs └── src │ ├── gui.rs │ └── main.rs ├── sfml-gui ├── Cargo.toml └── src │ ├── control.rs │ ├── gui.rs │ └── main.rs └── wasm-gui ├── Cargo.lock ├── Cargo.toml ├── buildAndServe.sh ├── index.html └── src └── lib.rs /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "rust" 5 | 6 | [analyzers.meta] 7 | msrv = "stable" 8 | 9 | [[analyzers]] 10 | name = "shell" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Install dependencies 17 | run: | 18 | sudo apt-get update 19 | sudo apt-get install libpthread-stubs0-dev libgl1-mesa-dev libx11-dev libx11-xcb-dev libxcb-image0-dev libxrandr-dev libxcb-randr0-dev libudev-dev libfreetype6-dev libglew-dev libjpeg8-dev libgpgme11-dev libsndfile1-dev libopenal-dev libjpeg62 libxcursor-dev cmake libclang-dev clang libsfml-dev 20 | - name: Build 21 | run: cargo build 22 | - name: Run tests 23 | run: cargo test 24 | - name: Run tests without default features 25 | run: cargo test --no-default-features 26 | - name: Run clippy 27 | uses: actions-rs/clippy-check@v1 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | format: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Format Rust code 36 | run: cargo fmt --all -- --check 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .DS_Store 3 | /roms 4 | flamegraph.svg 5 | bytes.sav 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | trailing_comma = "Never" 3 | match_block_trailing_comma = true -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "arbitrary-int" 16 | version = "1.2.7" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "c84fc003e338a6f69fbd4f7fe9f92b535ff13e9af8997f3b14b6ddff8b1df46d" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.4.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 25 | 26 | [[package]] 27 | name = "base64" 28 | version = "0.22.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 31 | 32 | [[package]] 33 | name = "bindgen" 34 | version = "0.63.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" 37 | dependencies = [ 38 | "bitflags 1.3.2", 39 | "cexpr", 40 | "clang-sys", 41 | "lazy_static", 42 | "lazycell", 43 | "log", 44 | "peeking_take_while", 45 | "proc-macro2", 46 | "quote", 47 | "regex", 48 | "rustc-hash", 49 | "shlex", 50 | "syn 1.0.109", 51 | "which", 52 | ] 53 | 54 | [[package]] 55 | name = "bitbybit" 56 | version = "1.3.2" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "fb157f9753a7cddfcf4a4f5fed928fbf4ce1b7b64b6bcc121d7a9f95d698997b" 59 | dependencies = [ 60 | "arbitrary-int", 61 | "proc-macro2", 62 | "quote", 63 | "syn 2.0.87", 64 | ] 65 | 66 | [[package]] 67 | name = "bitflags" 68 | version = "1.3.2" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 71 | 72 | [[package]] 73 | name = "bitflags" 74 | version = "2.6.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 77 | 78 | [[package]] 79 | name = "bumpalo" 80 | version = "3.16.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 83 | 84 | [[package]] 85 | name = "c_utf8" 86 | version = "0.1.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "f747ed2575d426b7cbf0fcba5872db319a600d597391c339779a3d9835d1ea4d" 89 | dependencies = [ 90 | "version_check", 91 | ] 92 | 93 | [[package]] 94 | name = "cc" 95 | version = "1.2.1" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" 98 | dependencies = [ 99 | "shlex", 100 | ] 101 | 102 | [[package]] 103 | name = "cexpr" 104 | version = "0.6.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 107 | dependencies = [ 108 | "nom", 109 | ] 110 | 111 | [[package]] 112 | name = "cfg-if" 113 | version = "1.0.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 116 | 117 | [[package]] 118 | name = "clang-sys" 119 | version = "1.8.1" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 122 | dependencies = [ 123 | "glob", 124 | "libc", 125 | "libloading", 126 | ] 127 | 128 | [[package]] 129 | name = "cmake" 130 | version = "0.1.51" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" 133 | dependencies = [ 134 | "cc", 135 | ] 136 | 137 | [[package]] 138 | name = "console_error_panic_hook" 139 | version = "0.1.7" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 142 | dependencies = [ 143 | "cfg-if", 144 | "wasm-bindgen", 145 | ] 146 | 147 | [[package]] 148 | name = "either" 149 | version = "1.13.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 152 | 153 | [[package]] 154 | name = "errno" 155 | version = "0.3.9" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 158 | dependencies = [ 159 | "libc", 160 | "windows-sys", 161 | ] 162 | 163 | [[package]] 164 | name = "gbrs-core" 165 | version = "0.2.0" 166 | dependencies = [ 167 | "smallvec", 168 | "spin", 169 | ] 170 | 171 | [[package]] 172 | name = "gbrs-libretro" 173 | version = "0.1.0" 174 | dependencies = [ 175 | "gbrs-core", 176 | "libretro-rs", 177 | "spin", 178 | ] 179 | 180 | [[package]] 181 | name = "gbrs-sdl-gui" 182 | version = "0.2.0" 183 | dependencies = [ 184 | "gbrs-core", 185 | "sdl2", 186 | ] 187 | 188 | [[package]] 189 | name = "gbrs-sfml-gui" 190 | version = "0.2.0" 191 | dependencies = [ 192 | "gbrs-core", 193 | "sfml", 194 | "spin", 195 | ] 196 | 197 | [[package]] 198 | name = "gbrs-wasm-gui" 199 | version = "0.1.0" 200 | dependencies = [ 201 | "base64", 202 | "console_error_panic_hook", 203 | "gbrs-core", 204 | "wasm-bindgen", 205 | "web-sys", 206 | ] 207 | 208 | [[package]] 209 | name = "glob" 210 | version = "0.3.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 213 | 214 | [[package]] 215 | name = "home" 216 | version = "0.5.9" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 219 | dependencies = [ 220 | "windows-sys", 221 | ] 222 | 223 | [[package]] 224 | name = "js-sys" 225 | version = "0.3.77" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 228 | dependencies = [ 229 | "once_cell", 230 | "wasm-bindgen", 231 | ] 232 | 233 | [[package]] 234 | name = "lazy_static" 235 | version = "1.5.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 238 | 239 | [[package]] 240 | name = "lazycell" 241 | version = "1.3.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 244 | 245 | [[package]] 246 | name = "libc" 247 | version = "0.2.164" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" 250 | 251 | [[package]] 252 | name = "libflac-sys" 253 | version = "0.3.1" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "5b0d9b582c1affe84b051d5a0faf3665e4da0c0b7b0e3b391213e3a5a670365b" 256 | dependencies = [ 257 | "cmake", 258 | "libc", 259 | ] 260 | 261 | [[package]] 262 | name = "libloading" 263 | version = "0.8.5" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" 266 | dependencies = [ 267 | "cfg-if", 268 | "windows-targets", 269 | ] 270 | 271 | [[package]] 272 | name = "libretro-rs" 273 | version = "0.2.0-SNAPSHOT" 274 | source = "git+https://github.com/libretro-rs/libretro-rs.git#8ebf60c023d2f1d36e41eb8beec3ff86524ca500" 275 | dependencies = [ 276 | "arbitrary-int", 277 | "bitbybit", 278 | "c_utf8", 279 | "libretro-rs-ffi", 280 | ] 281 | 282 | [[package]] 283 | name = "libretro-rs-ffi" 284 | version = "0.1.0" 285 | source = "git+https://github.com/libretro-rs/libretro-rs.git#8ebf60c023d2f1d36e41eb8beec3ff86524ca500" 286 | dependencies = [ 287 | "bindgen", 288 | "sptr", 289 | ] 290 | 291 | [[package]] 292 | name = "link-cplusplus" 293 | version = "1.0.9" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" 296 | dependencies = [ 297 | "cc", 298 | ] 299 | 300 | [[package]] 301 | name = "linux-raw-sys" 302 | version = "0.4.14" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 305 | 306 | [[package]] 307 | name = "lock_api" 308 | version = "0.4.12" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 311 | dependencies = [ 312 | "autocfg", 313 | "scopeguard", 314 | ] 315 | 316 | [[package]] 317 | name = "log" 318 | version = "0.4.22" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 321 | 322 | [[package]] 323 | name = "memchr" 324 | version = "2.7.4" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 327 | 328 | [[package]] 329 | name = "minimal-lexical" 330 | version = "0.2.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 333 | 334 | [[package]] 335 | name = "nom" 336 | version = "7.1.3" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 339 | dependencies = [ 340 | "memchr", 341 | "minimal-lexical", 342 | ] 343 | 344 | [[package]] 345 | name = "num-traits" 346 | version = "0.2.19" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 349 | dependencies = [ 350 | "autocfg", 351 | ] 352 | 353 | [[package]] 354 | name = "once_cell" 355 | version = "1.20.2" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 358 | 359 | [[package]] 360 | name = "peeking_take_while" 361 | version = "0.1.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 364 | 365 | [[package]] 366 | name = "pkg-config" 367 | version = "0.3.31" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 370 | 371 | [[package]] 372 | name = "proc-macro2" 373 | version = "1.0.89" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 376 | dependencies = [ 377 | "unicode-ident", 378 | ] 379 | 380 | [[package]] 381 | name = "profiling" 382 | version = "0.1.0" 383 | dependencies = [ 384 | "gbrs-core", 385 | ] 386 | 387 | [[package]] 388 | name = "quote" 389 | version = "1.0.37" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 392 | dependencies = [ 393 | "proc-macro2", 394 | ] 395 | 396 | [[package]] 397 | name = "regex" 398 | version = "1.11.1" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 401 | dependencies = [ 402 | "aho-corasick", 403 | "memchr", 404 | "regex-automata", 405 | "regex-syntax", 406 | ] 407 | 408 | [[package]] 409 | name = "regex-automata" 410 | version = "0.4.9" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 413 | dependencies = [ 414 | "aho-corasick", 415 | "memchr", 416 | "regex-syntax", 417 | ] 418 | 419 | [[package]] 420 | name = "regex-syntax" 421 | version = "0.8.5" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 424 | 425 | [[package]] 426 | name = "rustc-hash" 427 | version = "1.1.0" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 430 | 431 | [[package]] 432 | name = "rustix" 433 | version = "0.38.41" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" 436 | dependencies = [ 437 | "bitflags 2.6.0", 438 | "errno", 439 | "libc", 440 | "linux-raw-sys", 441 | "windows-sys", 442 | ] 443 | 444 | [[package]] 445 | name = "rustversion" 446 | version = "1.0.19" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 449 | 450 | [[package]] 451 | name = "scopeguard" 452 | version = "1.2.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 455 | 456 | [[package]] 457 | name = "sdl2" 458 | version = "0.37.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" 461 | dependencies = [ 462 | "bitflags 1.3.2", 463 | "lazy_static", 464 | "libc", 465 | "sdl2-sys", 466 | ] 467 | 468 | [[package]] 469 | name = "sdl2-sys" 470 | version = "0.37.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" 473 | dependencies = [ 474 | "cfg-if", 475 | "cmake", 476 | "libc", 477 | "version-compare", 478 | ] 479 | 480 | [[package]] 481 | name = "sfml" 482 | version = "0.24.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "941169c8be33fd81c006591c0dff6056f94bca0bff6057a262ba946bdb6dc0ed" 485 | dependencies = [ 486 | "bitflags 2.6.0", 487 | "cc", 488 | "cmake", 489 | "libflac-sys", 490 | "link-cplusplus", 491 | "num-traits", 492 | "pkg-config", 493 | "widestring", 494 | ] 495 | 496 | [[package]] 497 | name = "shlex" 498 | version = "1.3.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 501 | 502 | [[package]] 503 | name = "smallvec" 504 | version = "1.15.0" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 507 | 508 | [[package]] 509 | name = "spin" 510 | version = "0.9.8" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 513 | dependencies = [ 514 | "lock_api", 515 | ] 516 | 517 | [[package]] 518 | name = "sptr" 519 | version = "0.3.2" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" 522 | 523 | [[package]] 524 | name = "syn" 525 | version = "1.0.109" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 528 | dependencies = [ 529 | "proc-macro2", 530 | "quote", 531 | "unicode-ident", 532 | ] 533 | 534 | [[package]] 535 | name = "syn" 536 | version = "2.0.87" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 539 | dependencies = [ 540 | "proc-macro2", 541 | "quote", 542 | "unicode-ident", 543 | ] 544 | 545 | [[package]] 546 | name = "unicode-ident" 547 | version = "1.0.14" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 550 | 551 | [[package]] 552 | name = "version-compare" 553 | version = "0.1.1" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 556 | 557 | [[package]] 558 | name = "version_check" 559 | version = "0.1.5" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 562 | 563 | [[package]] 564 | name = "wasm-bindgen" 565 | version = "0.2.100" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 568 | dependencies = [ 569 | "cfg-if", 570 | "once_cell", 571 | "rustversion", 572 | "wasm-bindgen-macro", 573 | ] 574 | 575 | [[package]] 576 | name = "wasm-bindgen-backend" 577 | version = "0.2.100" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 580 | dependencies = [ 581 | "bumpalo", 582 | "log", 583 | "proc-macro2", 584 | "quote", 585 | "syn 2.0.87", 586 | "wasm-bindgen-shared", 587 | ] 588 | 589 | [[package]] 590 | name = "wasm-bindgen-macro" 591 | version = "0.2.100" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 594 | dependencies = [ 595 | "quote", 596 | "wasm-bindgen-macro-support", 597 | ] 598 | 599 | [[package]] 600 | name = "wasm-bindgen-macro-support" 601 | version = "0.2.100" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 604 | dependencies = [ 605 | "proc-macro2", 606 | "quote", 607 | "syn 2.0.87", 608 | "wasm-bindgen-backend", 609 | "wasm-bindgen-shared", 610 | ] 611 | 612 | [[package]] 613 | name = "wasm-bindgen-shared" 614 | version = "0.2.100" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 617 | dependencies = [ 618 | "unicode-ident", 619 | ] 620 | 621 | [[package]] 622 | name = "web-sys" 623 | version = "0.3.77" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 626 | dependencies = [ 627 | "js-sys", 628 | "wasm-bindgen", 629 | ] 630 | 631 | [[package]] 632 | name = "which" 633 | version = "4.4.2" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 636 | dependencies = [ 637 | "either", 638 | "home", 639 | "once_cell", 640 | "rustix", 641 | ] 642 | 643 | [[package]] 644 | name = "widestring" 645 | version = "1.1.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" 648 | 649 | [[package]] 650 | name = "windows-sys" 651 | version = "0.52.0" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 654 | dependencies = [ 655 | "windows-targets", 656 | ] 657 | 658 | [[package]] 659 | name = "windows-targets" 660 | version = "0.52.6" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 663 | dependencies = [ 664 | "windows_aarch64_gnullvm", 665 | "windows_aarch64_msvc", 666 | "windows_i686_gnu", 667 | "windows_i686_gnullvm", 668 | "windows_i686_msvc", 669 | "windows_x86_64_gnu", 670 | "windows_x86_64_gnullvm", 671 | "windows_x86_64_msvc", 672 | ] 673 | 674 | [[package]] 675 | name = "windows_aarch64_gnullvm" 676 | version = "0.52.6" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 679 | 680 | [[package]] 681 | name = "windows_aarch64_msvc" 682 | version = "0.52.6" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 685 | 686 | [[package]] 687 | name = "windows_i686_gnu" 688 | version = "0.52.6" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 691 | 692 | [[package]] 693 | name = "windows_i686_gnullvm" 694 | version = "0.52.6" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 697 | 698 | [[package]] 699 | name = "windows_i686_msvc" 700 | version = "0.52.6" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 703 | 704 | [[package]] 705 | name = "windows_x86_64_gnu" 706 | version = "0.52.6" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 709 | 710 | [[package]] 711 | name = "windows_x86_64_gnullvm" 712 | version = "0.52.6" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 715 | 716 | [[package]] 717 | name = "windows_x86_64_msvc" 718 | version = "0.52.6" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 721 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["core", "libretro", "profiling", "sdl-gui", "sfml-gui", "wasm-gui"] 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2022 Adam Soutar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /NOTE.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | If you lock a Mac (CMD+CTRL+Q) while gbrs is running from `cargo run --release`, 4 | `cargo` will segfault. 5 | 6 | Dr. Mario locks up, seemingly looking for the GPU to enter OAMSearch status, 7 | but when it checks we're always reporting that we're in VBlank 8 | 9 | F-1 Race seems to run too slow - maybe the timers aren't right? 10 | 11 | Donkey Kong's audio is waaaaay too slow - again a timer issue? 12 | 13 | Space Invaders and Zelda seem to have the same issue where they can only make 14 | certain APU Channel 4 sounds once - Space Invaders only makes one shot fire 15 | noise, and Zelda only makes one sword slash noise. I think it might be because 16 | I haven't implemented _reading_ from APU channel addresses. 17 | 18 | ## Optimisation ideas 19 | 20 | In Memory::Step, which is 10% of runtime according to `cargo-flamegraph`, can 21 | probably have its loops unrolled slightly. Instead of running per-cycle, we 22 | could probably step timers and the like by doing addition rather than repeated 23 | increments. ✅ 24 | 25 | MBCs probably do not need to be stepped per-cycle either. They can likely be 26 | stepped per frame, or _even per second_ or something, and still be fine. 27 | This step is only for save files and real-time clocks (not implemented). 28 | MBC::Step _may_ currently be slow due to MBCs being allocated on the heap and 29 | using indirection due to traits. - This turned out not to be very important. 30 | Even not stepping an MBC at all barely impacts performance. 31 | 32 | There may be optimisation to be found in the fact that, if we have a sprite 33 | pixel, there is no need to go and calculate a background pixel colour. - This 34 | isn't terribly useful as sprites don't cover tonnes of the screen. 35 | 36 | The screen buffer likely does not need to be fully copied every frame. 37 | Since we're not at all multi-threaded (yet?), frame data will not be modified 38 | during rendering. - This also doesn't seem to be much quicker. 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gbrs 2 | 3 | A Rust GameBoy emulator! 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
TetrisZelda: Link's Awakening
Super Mario LandSuper Mario Land 2
Galaga & GalaxianMortal Kombat
Pac-ManAlleyway
Space InvadersRoad Rash
Donkey KongFrogger
dmg-acid2
57 | 58 | ## Support 59 | 60 | gbrs supports: 61 | 62 | - Mid-frame scanline effects (required for games like Road Rash) 63 | - The Window (a GPU feature required for Pac Man and Zelda) 64 | - Cycle-accurate CPU & counters 65 | - Save files & saved games (Zelda & Super Mario Land 2 use these) 66 | - The Window internal line counter (an unusual quirk required for perfect DMG-ACID2 rendering) 67 | - LCD Stat interrupt bug (a bug present on the real Gameboy hardware required for Road Rash) 68 | - Memory Board Controller 1 (MBCs are required for some more complex games) 69 | - Memory Board Controller 2 70 | - Memory Board Controller 3 (Real-time clock WIP) 71 | - Sound! 72 | 73 | & more! 74 | 75 | ## Progress so far 76 | 77 | I'm still working on gbrs (and having a **_tonne_** of fun doing it!). 78 | 79 | The main thing(s) I'm working on: 80 | 81 | - MBC3 RTC for real-world timekeeping in Pokemon 82 | - Laying the foundations for GameBoy Color support 83 | - Performance optimisations for bare-metal ports 84 | 85 | ## Building from source 86 | 87 | gbrs is not yet finished enough to distribute binaries, but if you want to try it out: 88 | 89 | The repo contains ports for multiple graphics backends. SDL is the easiest to build. 90 | 91 | ### SDL 92 | 93 | The SDL port comes with everything you need to compile & run in one 94 | command. If you experience issues with screen tearing or cracking 95 | sound, check out the SFML port instead. 96 | 97 | ```bash 98 | git clone https://github.com/adamsoutar/gbrs 99 | cd gbrs/sdl-gui 100 | cargo run --release ROM_PATH 101 | ``` 102 | 103 | (Replace ROM_PATH with the path to a .gb file) 104 | 105 | ### SFML 106 | 107 | You'll need SFML set up, which you can find instructions for [here](https://github.com/jeremyletang/rust-sfml/wiki). 108 | 109 | Afterwards, in a terminal, you can execute these commands, assuming you have a 110 | [Rust](https://rustlang.org) toolchain installed. 111 | 112 | ``` 113 | git clone https://github.com/adamsoutar/gbrs 114 | cd gbrs/sfml-gui 115 | cargo run --release ROM_PATH 116 | ``` 117 | 118 | ## Ports to non-PC platforms 119 | 120 | gbrs is written to be ported to other platforms. Its default GUIs for Windows, 121 | macOS and Linux are just modules that it doesn't _have_ to use. 122 | 123 | You can port [gbrs-core](./core) to almost anything - especially since it 124 | supports running _without_ the Rust StdLib. 125 | 126 | All a port needs to do is: 127 | 128 | ```rust 129 | use gbrs_core::cpu::Cpu; 130 | 131 | let mut gameboy = Cpu::from_rom_bytes( 132 | include_bytes!("./tetris.gb").to_vec() 133 | ); 134 | 135 | // Each frame: 136 | gameboy.step_one_frame(); 137 | draw_screen(&gameboy.gpu.finished_frame); 138 | // (where draw_screen is a platform-specific function left to the reader) 139 | ``` 140 | 141 | --- 142 | 143 |
By Adam Soutar
144 | -------------------------------------------------------------------------------- /assets/alleyway.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/alleyway.gif -------------------------------------------------------------------------------- /assets/dmg-acid2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/dmg-acid2.jpg -------------------------------------------------------------------------------- /assets/donkeykong.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/donkeykong.gif -------------------------------------------------------------------------------- /assets/frogger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/frogger.gif -------------------------------------------------------------------------------- /assets/galaga.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/galaga.gif -------------------------------------------------------------------------------- /assets/mario.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/mario.gif -------------------------------------------------------------------------------- /assets/mario2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/mario2.gif -------------------------------------------------------------------------------- /assets/mortalkombat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/mortalkombat.gif -------------------------------------------------------------------------------- /assets/pacman.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/pacman.gif -------------------------------------------------------------------------------- /assets/roadrash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/roadrash.gif -------------------------------------------------------------------------------- /assets/spaceinvaders.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/spaceinvaders.gif -------------------------------------------------------------------------------- /assets/tetris.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/tetris.gif -------------------------------------------------------------------------------- /assets/zelda.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/assets/zelda.gif -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gbrs-core" 3 | version = "0.2.0" 4 | authors = ["Adam Soutar "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | smallvec = "1.15.0" 9 | spin = { version = "0.9.8", features = ["spin_mutex"] } 10 | 11 | [features] 12 | default = ["std", "sound"] 13 | std = [] 14 | sound = [] 15 | -------------------------------------------------------------------------------- /core/src/alu.rs: -------------------------------------------------------------------------------- 1 | // CPU Arithmetic Logic Unit 2 | use crate::cpu::Cpu; 3 | 4 | const ALU_ADD: u8 = 0b000; 5 | const ALU_ADC: u8 = 0b001; 6 | const ALU_SUB: u8 = 0b010; 7 | const ALU_SBC: u8 = 0b011; 8 | const ALU_AND: u8 = 0b100; 9 | const ALU_XOR: u8 = 0b101; 10 | const ALU_OR: u8 = 0b110; 11 | const ALU_CP: u8 = 0b111; 12 | 13 | impl Cpu { 14 | pub fn alu(&mut self, operation: u8, n: u8) { 15 | let a = self.regs.a; 16 | let c = self.regs.get_carry_flag(); 17 | 18 | match operation { 19 | ALU_ADD => { 20 | // ADD 21 | let res = a.wrapping_add(n); 22 | self.regs.set_carry_flag((a as u16 + n as u16 > 0xFF) as u8); 23 | self.regs.set_half_carry_flag( 24 | ((a & 0x0F) + (n & 0x0F) > 0x0F) as u8, 25 | ); 26 | self.regs.set_zero_flag((res == 0) as u8); 27 | self.regs.set_operation_flag(0); 28 | self.regs.a = res; 29 | }, 30 | ALU_ADC => { 31 | // ADC 32 | let res = a.wrapping_add(n).wrapping_add(c); 33 | self.regs.set_carry_flag( 34 | (a as u16 + n as u16 + c as u16 > 0xFF) as u8, 35 | ); 36 | self.regs.set_half_carry_flag( 37 | ((a & 0x0F) + (n & 0x0F) + c > 0x0F) as u8, 38 | ); 39 | self.regs.set_zero_flag((res == 0) as u8); 40 | self.regs.set_operation_flag(0); 41 | self.regs.a = res; 42 | }, 43 | ALU_SUB => { 44 | // SUB 45 | let res = a.wrapping_sub(n); 46 | self.regs.set_carry_flag((a < n) as u8); 47 | self.regs 48 | .set_half_carry_flag(((a & 0x0F) < (n & 0x0F)) as u8); 49 | self.regs.set_operation_flag(1); 50 | self.regs.set_zero_flag((res == 0) as u8); 51 | self.regs.a = res; 52 | }, 53 | ALU_SBC => { 54 | // SBC 55 | let res = a.wrapping_sub(n).wrapping_sub(c); 56 | self.regs 57 | .set_carry_flag(((a as u16) < (n as u16 + c as u16)) as u8); 58 | self.regs 59 | .set_half_carry_flag(((a & 0x0F) < (n & 0x0F) + c) as u8); 60 | self.regs.set_operation_flag(1); 61 | self.regs.set_zero_flag((res == 0) as u8); 62 | self.regs.a = res; 63 | }, 64 | ALU_AND => { 65 | // AND 66 | let res = a & n; 67 | self.regs.set_carry_flag(0); 68 | self.regs.set_half_carry_flag(1); 69 | self.regs.set_operation_flag(0); 70 | self.regs.set_zero_flag((res == 0) as u8); 71 | self.regs.a = res; 72 | }, 73 | ALU_XOR => { 74 | // XOR 75 | let res = a ^ n; 76 | self.regs.set_carry_flag(0); 77 | self.regs.set_half_carry_flag(0); 78 | self.regs.set_operation_flag(0); 79 | self.regs.set_zero_flag((res == 0) as u8); 80 | self.regs.a = res; 81 | }, 82 | ALU_OR => { 83 | // OR 84 | let res = a | n; 85 | self.regs.set_carry_flag(0); 86 | self.regs.set_half_carry_flag(0); 87 | self.regs.set_operation_flag(0); 88 | self.regs.set_zero_flag((res == 0) as u8); 89 | self.regs.a = res; 90 | }, 91 | ALU_CP => { 92 | // CP ("Compare") 93 | // It's a subtraction in terms of flags, but it throws away the result 94 | self.alu(ALU_SUB, n); 95 | self.regs.a = a; 96 | }, 97 | _ => panic!("Unsupported ALU operation {:b}", operation), 98 | } 99 | } 100 | 101 | pub fn alu_dec(&mut self, n: u8) -> u8 { 102 | let r = n.wrapping_sub(1); 103 | self.regs 104 | .set_half_carry_flag((n.trailing_zeros() >= 4) as u8); 105 | self.regs.set_operation_flag(1); 106 | self.regs.set_zero_flag((r == 0) as u8); 107 | r 108 | } 109 | pub fn alu_inc(&mut self, n: u8) -> u8 { 110 | let r = n.wrapping_add(1); 111 | self.regs 112 | .set_half_carry_flag(((n & 0x0f) + 0x01 > 0x0f) as u8); 113 | self.regs.set_operation_flag(0); 114 | self.regs.set_zero_flag((r == 0) as u8); 115 | r 116 | } 117 | pub fn alu_add_hl(&mut self, n: u16) { 118 | let hl = self.regs.get_hl(); 119 | let r = hl.wrapping_add(n); 120 | 121 | self.regs.set_carry_flag((hl > 0xffff - n) as u8); 122 | self.regs 123 | .set_half_carry_flag(((hl & 0x0fff) + (n & 0x0fff) > 0x0fff) as u8); 124 | self.regs.set_operation_flag(0); 125 | 126 | self.regs.set_hl(r); 127 | } 128 | 129 | // R for "Rotate" (Bitshift) 130 | fn alu_rlc(&mut self, n: u8) -> u8 { 131 | let c = (n & 0b10000000) >> 7; 132 | let r = (n << 1) | c; 133 | self.regs.set_carry_flag(c); 134 | self.regs.set_operation_flag(0); 135 | self.regs.set_half_carry_flag(0); 136 | self.regs.set_zero_flag((r == 0) as u8); 137 | r 138 | } 139 | fn alu_rl(&mut self, n: u8) -> u8 { 140 | let c = (n & 0b10000000) >> 7; 141 | let r = (n << 1) | self.regs.get_carry_flag(); 142 | self.regs.set_carry_flag(c); 143 | self.regs.set_operation_flag(0); 144 | self.regs.set_half_carry_flag(0); 145 | self.regs.set_zero_flag((r == 0) as u8); 146 | r 147 | } 148 | fn alu_rrc(&mut self, n: u8) -> u8 { 149 | let c = n & 1; 150 | let r = (n >> 1) | (c << 7); 151 | self.regs.set_carry_flag(c); 152 | self.regs.set_operation_flag(0); 153 | self.regs.set_half_carry_flag(0); 154 | self.regs.set_zero_flag((r == 0) as u8); 155 | r 156 | } 157 | fn alu_rr(&mut self, n: u8) -> u8 { 158 | let c = n & 1; 159 | let r = (n >> 1) | (self.regs.get_carry_flag() << 7); 160 | self.regs.set_carry_flag(c); 161 | self.regs.set_half_carry_flag(0); 162 | self.regs.set_operation_flag(0); 163 | self.regs.set_zero_flag((r == 0) as u8); 164 | r 165 | } 166 | 167 | fn alu_sla(&mut self, n: u8) -> u8 { 168 | let c = (n & 0x80) >> 7; 169 | let r = n << 1; 170 | self.regs.set_carry_flag(c); 171 | self.regs.set_half_carry_flag(0); 172 | self.regs.set_operation_flag(0); 173 | self.regs.set_zero_flag((r == 0) as u8); 174 | r 175 | } 176 | fn alu_sra(&mut self, n: u8) -> u8 { 177 | let c = n & 1; 178 | let r = (n >> 1) | (n & 0x80); 179 | self.regs.set_carry_flag(c); 180 | self.regs.set_half_carry_flag(0); 181 | self.regs.set_operation_flag(0); 182 | self.regs.set_zero_flag((r == 0) as u8); 183 | r 184 | } 185 | pub fn alu_special_rotate(&mut self, right: bool, n: u8) -> u8 { 186 | if right { 187 | self.alu_sra(n) 188 | } else { 189 | self.alu_sla(n) 190 | } 191 | } 192 | 193 | pub fn alu_srl(&mut self, n: u8) -> u8 { 194 | let c = n & 1; 195 | let r = n >> 1; 196 | self.regs.set_carry_flag(c); 197 | self.regs.set_half_carry_flag(0); 198 | self.regs.set_operation_flag(0); 199 | self.regs.set_zero_flag((r == 0) as u8); 200 | r 201 | } 202 | 203 | pub fn alu_rotate_val(&mut self, right: bool, carry: bool, a: u8) -> u8 { 204 | if !right { 205 | if carry { 206 | self.alu_rlc(a) 207 | } else { 208 | self.alu_rl(a) 209 | } 210 | } else { 211 | if carry { 212 | self.alu_rrc(a) 213 | } else { 214 | self.alu_rr(a) 215 | } 216 | } 217 | } 218 | pub fn alu_rotate(&mut self, right: bool, carry: bool) { 219 | let a = self.regs.a; 220 | self.regs.a = self.alu_rotate_val(right, carry, a); 221 | } 222 | 223 | // DAA is proper weird. For this one, I had to look at: 224 | // https://github.com/mohanson/gameboy/blob/master/src/cpu.rs#L325 225 | pub fn alu_daa(&mut self) { 226 | let mut a = self.regs.a; 227 | 228 | let mut adjust = if self.regs.get_carry_flag() == 1 { 229 | 0x60 230 | } else { 231 | 0 232 | }; 233 | 234 | if self.regs.get_half_carry_flag() == 1 { 235 | adjust |= 0x06; 236 | } 237 | 238 | if self.regs.get_operation_flag() == 0 { 239 | if a & 0x0f > 0x09 { 240 | adjust |= 0x06; 241 | }; 242 | if a > 0x99 { 243 | adjust |= 0x60; 244 | }; 245 | a = a.wrapping_add(adjust); 246 | } else { 247 | a = a.wrapping_sub(adjust); 248 | } 249 | 250 | self.regs.set_carry_flag((adjust >= 0x60) as u8); 251 | self.regs.set_half_carry_flag(0); 252 | self.regs.set_zero_flag((a == 0) as u8); 253 | 254 | self.regs.a = a; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /core/src/callbacks.rs: -------------------------------------------------------------------------------- 1 | // This allows ports to register functions for things like logging as well as 2 | // saving/loading battery-backed RAM. 3 | 4 | #[cfg(not(feature = "std"))] 5 | use alloc::{vec, vec::Vec}; 6 | use spin::mutex::spin::SpinMutex; 7 | #[cfg(feature = "std")] 8 | use std::{fs, io::Read, path::PathBuf}; 9 | 10 | pub type LogCallback = fn(log_str: &str); 11 | pub type SaveCallback = 12 | fn(game_name: &str, rom_path: &str, save_data: &Vec); 13 | pub type LoadCallback = 14 | fn(game_name: &str, rom_path: &str, expected_size: usize) -> Vec; 15 | 16 | #[derive(Clone)] 17 | pub struct Callbacks { 18 | pub log: LogCallback, 19 | pub save: SaveCallback, 20 | pub load: LoadCallback, 21 | } 22 | 23 | #[cfg(feature = "std")] 24 | fn get_save_file_path(rom_path: &str) -> String { 25 | let mut sav_path = PathBuf::from(rom_path); 26 | sav_path.set_extension("sav"); 27 | 28 | sav_path.to_string_lossy().to_string() 29 | } 30 | 31 | #[cfg(feature = "std")] 32 | pub static CALLBACKS: SpinMutex = SpinMutex::new(Callbacks { 33 | log: |log_str| println!("{}", log_str), 34 | save: |_game_name, rom_path, save_data| { 35 | let save_path = get_save_file_path(rom_path); 36 | fs::write(&save_path, save_data).expect("Failed to write save file"); 37 | }, 38 | load: |_game_name, rom_path, expected_size| { 39 | let save_path = get_save_file_path(rom_path); 40 | let mut buffer = vec![]; 41 | let file_result = fs::File::open(save_path); 42 | 43 | if let Ok(mut file) = file_result { 44 | file.read_to_end(&mut buffer) 45 | .expect("Unable to read save file"); 46 | buffer 47 | } else { 48 | // The save file likely does not exist 49 | vec![0; expected_size] 50 | } 51 | }, 52 | }); 53 | 54 | #[cfg(not(feature = "std"))] 55 | pub static CALLBACKS: SpinMutex = SpinMutex::new(Callbacks { 56 | log: |_log_str| {}, 57 | save: |_game_name, _rom_path, _save_data| {}, 58 | load: |_game_name, _rom_path, expected_size| vec![0; expected_size], 59 | }); 60 | 61 | pub fn set_callbacks(cbs: Callbacks) { 62 | *CALLBACKS.lock() = cbs; 63 | } 64 | -------------------------------------------------------------------------------- /core/src/cartridge.rs: -------------------------------------------------------------------------------- 1 | // Parses the cartridge header 2 | use crate::log; 3 | 4 | #[cfg(not(feature = "std"))] 5 | use alloc::{string::String, vec, vec::Vec}; 6 | 7 | #[derive(Clone)] 8 | pub enum CGBSupportType { 9 | None, 10 | Optional, 11 | Required, 12 | } 13 | 14 | #[derive(Clone)] 15 | pub struct Cartridge { 16 | pub title: String, 17 | pub rom_path: String, 18 | pub cart_type: u8, 19 | 20 | pub rom_size: usize, 21 | pub ram_size: usize, 22 | 23 | pub cgb_support: CGBSupportType, 24 | } 25 | 26 | impl Cartridge { 27 | pub fn parse(buffer: &Vec, rom_path: String) -> Cartridge { 28 | let title = get_title(buffer); 29 | 30 | let cart_type = buffer[0x0147]; 31 | 32 | let rom_size_id = buffer[0x0148]; 33 | let ram_size_id = buffer[0x0149]; 34 | 35 | let rom_size = 32768 << (rom_size_id as usize); 36 | let ram_size = match ram_size_id { 37 | 0 => 0, 38 | 1 => { 39 | log!("[WARN] Unofficial 2KB RAM size not used by any officially published game."); 40 | 2_048 41 | }, 42 | 2 => 8_192, 43 | 3 => 32_768, 44 | 4 => { 45 | log!("[WARN] RAM size is larger than a u16. Internal implementations such as BatteryBackedRam may fail."); 46 | 131_072 47 | }, 48 | 5 => 65_536, 49 | _ => { 50 | panic!("Unknown RAM size id for cartridge {:#04x}", ram_size_id) 51 | }, 52 | }; 53 | 54 | let cgb_support = match buffer[0x0143] { 55 | 0x80 => CGBSupportType::Optional, 56 | 0xC0 => CGBSupportType::Required, 57 | _ => CGBSupportType::None, 58 | }; 59 | 60 | Cartridge { 61 | title, 62 | rom_path, 63 | cart_type, 64 | rom_size, 65 | ram_size, 66 | cgb_support, 67 | } 68 | } 69 | } 70 | 71 | fn get_title(buffer: &Vec) -> String { 72 | let mut out_buff = vec![]; 73 | for i in 0x0134..=0x0143 { 74 | // A null byte terminates the title string 75 | // Also, later games have non-ascii values in their titles used for 76 | // flags like GameBoy Color support. 77 | if buffer[i] == 0 || buffer[i] > 0x7F { 78 | break; 79 | } 80 | out_buff.push(buffer[i]); 81 | } 82 | String::from_utf8(out_buff).expect("ROM title isn't valid UTF-8") 83 | } 84 | -------------------------------------------------------------------------------- /core/src/cgb_dma.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | pub enum CgbDmaType { 3 | GeneralPurpose, 4 | HBlank, 5 | } 6 | 7 | pub struct CgbDmaConfig { 8 | pub source: u16, 9 | pub dest: u16, 10 | pub dma_type: CgbDmaType, 11 | pub bytes_copied: u16, 12 | pub bytes_left: u16, 13 | pub transfer_done: bool, 14 | } 15 | 16 | impl CgbDmaConfig { 17 | pub fn set_config_byte(&mut self, value: u8) { 18 | self.transfer_done = false; 19 | self.dma_type = if value & 0x80 == 0x80 { 20 | CgbDmaType::HBlank 21 | } else { 22 | CgbDmaType::GeneralPurpose 23 | }; 24 | self.bytes_left = ((value & 0x7F) + 1) as u16 * 0x10; 25 | self.bytes_copied = 0; 26 | } 27 | pub fn get_config_byte(&self) -> u8 { 28 | if self.transfer_done { 29 | return 0xFF; 30 | } 31 | // TODO: Not sure this is quite the correct calculation 32 | ((self.bytes_left / 0x10) - 1) as u8 33 | } 34 | 35 | pub fn is_hblank_dma(&self) -> bool { 36 | self.dma_type == CgbDmaType::HBlank 37 | } 38 | 39 | pub fn get_source_upper(&self) -> u8 { 40 | (self.source >> 8) as u8 41 | } 42 | pub fn get_source_lower(&self) -> u8 { 43 | (self.source & 0xFF) as u8 44 | } 45 | pub fn set_source_upper(&mut self, value: u8) { 46 | self.source = (self.source & 0x00FF) | ((value as u16) << 8); 47 | } 48 | pub fn set_source_lower(&mut self, value: u8) { 49 | // Lower 4 bits of address are ignored 50 | self.source = (self.source & 0xFF00) | ((value & 0xF0) as u16); 51 | } 52 | 53 | pub fn get_dest_upper(&self) -> u8 { 54 | (self.dest >> 8) as u8 55 | } 56 | pub fn get_dest_lower(&self) -> u8 { 57 | (self.dest & 0xFF) as u8 58 | } 59 | pub fn set_dest_upper(&mut self, value: u8) { 60 | // This algo makes sure that the destination address is in the range 61 | // 0x8000 - 0x9FFF, ensuring that the destianation is in VRAM. 62 | self.dest = 63 | // Keep lower byte 64 | (self.dest & 0x00FF) | 65 | // Set upper byte 66 | ((( 67 | // Ignore upper 3 bits of value, making range 0x0000 - 0x1FFF 68 | (value & 0x1F) 69 | // Set the top bit, adding 0x8000 70 | | 0x80) as u16 71 | ) << 8); 72 | } 73 | pub fn set_dest_lower(&mut self, value: u8) { 74 | // Lower 4 bits of address are ignored 75 | self.dest = (self.dest & 0xFF00) | ((value & 0xF0) as u16); 76 | } 77 | 78 | pub fn new() -> CgbDmaConfig { 79 | CgbDmaConfig { 80 | source: 0, 81 | dest: 0, 82 | dma_type: CgbDmaType::GeneralPurpose, 83 | bytes_left: 0, 84 | bytes_copied: 0, 85 | transfer_done: false, 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /core/src/colour/bg_map_attributes.rs: -------------------------------------------------------------------------------- 1 | // Data pertaining to rendering coloured background/window tiles 2 | // Defined by writing to VRAM bank 1 0x9800 to 0x9FFF 3 | 4 | const BG_MAP_ATTRIBUTE_TABLE_SIZE: usize = 0x800; // 0x9FFF - 0x9800 + 0th addr 5 | 6 | #[derive(Clone, Copy)] 7 | pub struct BgMapAttributeEntry { 8 | pub priority: bool, 9 | pub y_flip: bool, 10 | pub x_flip: bool, 11 | // This is an unused bit, but the hardware keeps track of it 12 | // Games could use it for their own unusual hackery 13 | pub bit_four: bool, 14 | pub vram_bank: u8, // Either 0 or 1 15 | pub palette: u8, 16 | } 17 | 18 | impl BgMapAttributeEntry { 19 | pub fn as_u8(&self) -> u8 { 20 | let mut val = 0; 21 | if self.priority { 22 | val |= 0b1000_0000; 23 | } 24 | if self.y_flip { 25 | val |= 0b0100_0000; 26 | } 27 | if self.x_flip { 28 | val |= 0b0010_0000; 29 | } 30 | if self.bit_four { 31 | val |= 0b0001_0000; 32 | } 33 | val |= self.vram_bank << 3; 34 | val |= self.palette & 0b0000_0111; 35 | val 36 | } 37 | 38 | pub fn from_u8(val: u8) -> BgMapAttributeEntry { 39 | BgMapAttributeEntry { 40 | priority: (0b1000_0000 & val) > 0, 41 | y_flip: (0b0100_0000 & val) > 0, 42 | x_flip: (0b0010_0000 & val) > 0, 43 | bit_four: (0b0001_0000 & val) > 0, 44 | vram_bank: (0b0000_1000 & val) >> 3, 45 | palette: 0b0000_0111 & val, 46 | } 47 | } 48 | 49 | pub fn new() -> BgMapAttributeEntry { 50 | BgMapAttributeEntry { 51 | priority: false, 52 | y_flip: false, 53 | x_flip: false, 54 | bit_four: false, 55 | vram_bank: 0, 56 | palette: 0, 57 | } 58 | } 59 | } 60 | 61 | pub struct BgMapAttributeTable { 62 | entries: [BgMapAttributeEntry; BG_MAP_ATTRIBUTE_TABLE_SIZE], 63 | } 64 | 65 | impl BgMapAttributeTable { 66 | pub fn get_entry(&self, address: u16) -> BgMapAttributeEntry { 67 | self.entries[address as usize] 68 | } 69 | 70 | pub fn read(&self, address: u16) -> u8 { 71 | self.get_entry(address).as_u8() 72 | } 73 | 74 | pub fn write(&mut self, address: u16, value: u8) { 75 | self.entries[address as usize] = BgMapAttributeEntry::from_u8(value); 76 | } 77 | 78 | pub fn new() -> BgMapAttributeTable { 79 | BgMapAttributeTable { 80 | entries: [BgMapAttributeEntry::new(); BG_MAP_ATTRIBUTE_TABLE_SIZE], 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/src/colour/colour.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy)] 2 | pub struct Colour { 3 | pub red: u8, 4 | pub green: u8, 5 | pub blue: u8, 6 | } 7 | 8 | impl Colour { 9 | // Colour space conversion algo from 10 | // https://gamedev.stackexchange.com/a/196834 11 | pub fn from_16_bit_colour(val: u16) -> Colour { 12 | let mut red = ((val % 32) * 8) as u8; 13 | red = red + red / 32; 14 | let mut green = (((val / 32) % 32) * 8) as u8; 15 | green = green + green / 32; 16 | let mut blue = (((val / 1024) % 32) * 8) as u8; 17 | blue = blue + blue / 32; 18 | Colour { red, green, blue } 19 | } 20 | 21 | pub fn new(red: u8, green: u8, blue: u8) -> Colour { 22 | Colour { red, green, blue } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/colour/grey_shades.rs: -------------------------------------------------------------------------------- 1 | use super::colour::Colour; 2 | 3 | pub fn white() -> Colour { 4 | Colour::new(0xDD, 0xDD, 0xDD) 5 | } 6 | pub fn light_grey() -> Colour { 7 | Colour::new(0xAA, 0xAA, 0xAA) 8 | } 9 | pub fn dark_grey() -> Colour { 10 | Colour::new(0x88, 0x88, 0x88) 11 | } 12 | pub fn black() -> Colour { 13 | Colour::new(0x55, 0x55, 0x55) 14 | } 15 | 16 | pub fn colour_from_grey_shade_id(id: u8) -> Colour { 17 | match id { 18 | 0 => white(), 19 | 1 => light_grey(), 20 | 2 => dark_grey(), 21 | 3 => black(), 22 | _ => panic!("Invalid grey shade id {}", id), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/colour/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bg_map_attributes; 2 | pub mod colour; 3 | pub mod grey_shades; 4 | pub mod palette_ram; 5 | -------------------------------------------------------------------------------- /core/src/colour/palette_ram.rs: -------------------------------------------------------------------------------- 1 | use super::colour::Colour; 2 | use crate::{combine_u8, cpu::EmulationTarget, memory::ram::Ram}; 3 | 4 | fn palette_spec_read(address: u16, auto_increment: bool) -> u8 { 5 | // This should never be higher than 64 anyway, but let's be safe 6 | let lower_address = (address & 0b0001_1111) as u8; 7 | let auto_inc_bit = if auto_increment { 1 } else { 0 }; 8 | lower_address | (auto_inc_bit << 7) 9 | } 10 | 11 | fn palette_spec_write(address: &mut u16, value: u8, auto_increment: &mut bool) { 12 | *auto_increment = (value & 0b1000_0000) > 0; 13 | *address = (value & 0b0001_1111) as u16; 14 | } 15 | 16 | fn palette_data_write( 17 | ram: &mut Ram, 18 | address: &mut u16, 19 | value: u8, 20 | auto_increment: bool, 21 | ) { 22 | ram.write(*address, value); 23 | if auto_increment { 24 | *address = (*address + 1) % 64; 25 | } 26 | } 27 | 28 | pub struct PaletteRam { 29 | // If this is false, we're a DMG. 30 | cgb_features: bool, 31 | bg_palette_ram: Ram, 32 | bg_address: u16, 33 | bg_auto_increment: bool, 34 | obj_palette_ram: Ram, 35 | obj_address: u16, 36 | obj_auto_increment: bool, 37 | } 38 | 39 | impl PaletteRam { 40 | fn read_colour(&self, ram: &Ram, address: u16) -> Colour { 41 | let col0 = ram.read(address); 42 | let col1 = ram.read(address + 1); 43 | Colour::from_16_bit_colour(combine_u8!(col1, col0)) 44 | } 45 | 46 | pub fn get_bg_palette_colour( 47 | &self, 48 | palette_id: u16, 49 | colour_id: u16, 50 | ) -> Colour { 51 | let base_offset = 8 * palette_id; 52 | self.read_colour(&self.bg_palette_ram, base_offset + colour_id * 2) 53 | } 54 | 55 | pub fn get_obj_palette_colour( 56 | &self, 57 | palette_id: u16, 58 | colour_id: u16, 59 | ) -> Colour { 60 | let base_offset = 8 * palette_id; 61 | self.read_colour(&self.obj_palette_ram, base_offset + colour_id * 2) 62 | } 63 | 64 | pub fn raw_read(&self, address: u16) -> u8 { 65 | if !self.cgb_features { 66 | return 0xFF; 67 | } 68 | 69 | match address { 70 | 0xFF68 => { 71 | palette_spec_read(self.bg_address, self.bg_auto_increment) 72 | }, 73 | 0xFF69 => self.bg_palette_ram.read(self.bg_address), 74 | 75 | 0xFF6A => { 76 | palette_spec_read(self.obj_address, self.obj_auto_increment) 77 | }, 78 | 0xFF6B => self.obj_palette_ram.read(self.obj_address), 79 | 80 | _ => panic!("CGB Palette RAM read at {:#06x}", address), 81 | } 82 | } 83 | 84 | pub fn raw_write(&mut self, address: u16, value: u8) { 85 | if !self.cgb_features { 86 | return; 87 | } 88 | 89 | match address { 90 | 0xFF68 => palette_spec_write( 91 | &mut self.bg_address, 92 | value, 93 | &mut self.bg_auto_increment, 94 | ), 95 | 0xFF69 => palette_data_write( 96 | &mut self.bg_palette_ram, 97 | &mut self.bg_address, 98 | value, 99 | self.bg_auto_increment, 100 | ), 101 | 102 | 0xFF6A => palette_spec_write( 103 | &mut self.obj_address, 104 | value, 105 | &mut self.obj_auto_increment, 106 | ), 107 | 0xFF6B => palette_data_write( 108 | &mut self.obj_palette_ram, 109 | &mut self.obj_address, 110 | value, 111 | self.obj_auto_increment, 112 | ), 113 | 114 | _ => panic!( 115 | "CGB Palette RAM write at {:#06x} (value: {:#04x})", 116 | address, value 117 | ), 118 | } 119 | } 120 | 121 | pub fn new(target: &EmulationTarget) -> PaletteRam { 122 | PaletteRam { 123 | cgb_features: target.has_cgb_features(), 124 | // All background colours are white at boot 125 | bg_palette_ram: Ram::with_filled_value(64, 0xFF), 126 | bg_address: 0, 127 | bg_auto_increment: false, 128 | // Object memory is garbage on boot, but slot 0 is always 0 129 | obj_palette_ram: Ram::new(64), 130 | obj_address: 0, 131 | obj_auto_increment: false, 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /core/src/config.rs: -------------------------------------------------------------------------------- 1 | // Config for creating CPUs 2 | // This helps with ports 3 | use crate::memory::rom::Rom; 4 | 5 | #[derive(Clone)] 6 | pub struct Config { 7 | pub sound_buffer_size: usize, 8 | pub sound_sample_rate: usize, 9 | pub rom: Rom, 10 | } 11 | -------------------------------------------------------------------------------- /core/src/constants.rs: -------------------------------------------------------------------------------- 1 | // "WRAM" is Work RAM, not Wave RAM 2 | pub const WRAM_BANK_SIZE: usize = 4096; 3 | pub const VRAM_BANK_SIZE: usize = 8192; 4 | pub const HRAM_SIZE: usize = 127; 5 | pub const OAM_SIZE: usize = 160; 6 | pub const WAVE_RAM_SIZE: usize = 16; 7 | 8 | // Excluding invisible areas such as those above and to 9 | // the left of the screen 10 | pub const SCREEN_WIDTH: usize = 160; 11 | pub const SCREEN_HEIGHT: usize = 144; 12 | 13 | pub const SCREEN_BUFFER_SIZE: usize = SCREEN_WIDTH * SCREEN_HEIGHT; 14 | pub const SCREEN_RGBA_SLICE_SIZE: usize = SCREEN_BUFFER_SIZE * 4; 15 | 16 | pub const CLOCK_SPEED: usize = 4194304; 17 | pub const DEFAULT_FRAME_RATE: usize = 60; 18 | 19 | // The amount of sound samples we collect before firing them off for 20 | // playback. This number is essentially guessed. 21 | pub const SOUND_BUFFER_SIZE: usize = 2048; 22 | pub const SOUND_SAMPLE_RATE: usize = 48000; 23 | // The amount of APU step()s we should run before 24 | // we sample for audio. 25 | pub const APU_SAMPLE_CLOCKS: usize = CLOCK_SPEED / SOUND_SAMPLE_RATE; 26 | 27 | // MBC_ROM_START is 0 28 | pub const MBC_ROM_END: u16 = 0x7FFF; 29 | 30 | pub const MBC_RAM_START: u16 = 0xA000; 31 | pub const MBC_RAM_END: u16 = 0xBFFF; 32 | 33 | pub const VRAM_START: u16 = 0x8000; 34 | // For CGB BG Map Attribute Table 35 | pub const VRAM_BG_MAP_START: u16 = 0x9800; 36 | pub const VRAM_END: u16 = 0x9FFF; 37 | 38 | pub const WRAM_LOWER_BANK_START: u16 = 0xC000; 39 | pub const WRAM_LOWER_BANK_END: u16 = 0xCFFF; 40 | pub const WRAM_UPPER_BANK_START: u16 = 0xD000; 41 | pub const WRAM_UPPER_BANK_END: u16 = 0xDFFF; 42 | 43 | pub const ECHO_RAM_START: u16 = 0xE000; 44 | pub const ECHO_RAM_END: u16 = 0xFDFF; 45 | 46 | pub const OAM_START: u16 = 0xFE00; 47 | pub const OAM_END: u16 = 0xFE9F; 48 | 49 | pub const UNUSABLE_MEMORY_START: u16 = 0xFEA0; 50 | pub const UNUSABLE_MEMORY_END: u16 = 0xFEFF; 51 | 52 | pub const LINK_CABLE_SB: u16 = 0xFF01; 53 | pub const LINK_CABLE_SC: u16 = 0xFF02; 54 | 55 | pub const APU_START: u16 = 0xFF10; 56 | pub const APU_END: u16 = 0xFF3F; 57 | 58 | pub const WAVE_RAM_START: u16 = 0xFF30; 59 | pub const WAVE_RAM_END: u16 = 0xFF3F; 60 | 61 | pub const HRAM_START: u16 = 0xFF80; 62 | pub const HRAM_END: u16 = 0xFFFE; 63 | 64 | pub const LCD_DATA_START: u16 = 0xFF40; 65 | pub const LCD_DATA_END: u16 = 0xFF4C; 66 | 67 | pub const CGB_DMA_START: u16 = 0xFF51; 68 | pub const CGB_DMA_END: u16 = 0xFF55; 69 | 70 | pub const CGB_PALETTE_DATA_START: u16 = 0xFF68; 71 | pub const CGB_PALETTE_DATA_END: u16 = 0xFF6B; 72 | 73 | pub const INTERRUPT_ENABLE_ADDRESS: u16 = 0xFFFF; 74 | pub const INTERRUPT_FLAG_ADDRESS: u16 = 0xFF0F; 75 | 76 | pub const HALT_INSTRUCTION_OPCODE: u8 = 0x76; 77 | pub const SPEED_SWITCH_HALT_CYCLES: usize = 8200; 78 | 79 | pub mod gpu_timing { 80 | // Total line size incl. HBlank 81 | pub const HTOTAL: u16 = 456; 82 | 83 | // lx coordinate where Transfer begins 84 | pub const HTRANSFER_ON: u16 = 80; 85 | 86 | // Start of HBlank 87 | pub const HBLANK_ON: u16 = 252; 88 | 89 | // Total vertical lines incl. VBlank 90 | pub const VTOTAL: u8 = 154; 91 | // Start of VBlank 92 | pub const VBLANK_ON: u8 = 144; 93 | 94 | // Number of CPU cycles it takes to do a DMA 95 | pub const DMA_CYCLES: u8 = 160; 96 | } 97 | -------------------------------------------------------------------------------- /core/src/gpu.rs: -------------------------------------------------------------------------------- 1 | use crate::cgb_dma::CgbDmaConfig; 2 | use crate::colour::colour::Colour; 3 | use crate::colour::grey_shades; 4 | use crate::colour::grey_shades::colour_from_grey_shade_id; 5 | use crate::combine_u8; 6 | use crate::constants::*; 7 | use crate::interrupts::*; 8 | use crate::lcd::*; 9 | use crate::log; 10 | use crate::memory::memory::Memory; 11 | use crate::memory::ram::Ram; 12 | 13 | use smallvec::SmallVec; 14 | 15 | #[derive(Clone)] 16 | pub struct Sprite { 17 | pub y_pos: i32, 18 | pub x_pos: i32, 19 | pub pattern_id: u8, 20 | 21 | pub above_bg: bool, 22 | pub y_flip: bool, 23 | pub x_flip: bool, 24 | pub use_palette_0: bool, 25 | 26 | // CGB-specific attributes 27 | pub use_upper_vram_bank: bool, 28 | pub cgb_palette: u8, 29 | } 30 | 31 | pub struct Gpu { 32 | cgb_features: bool, 33 | // This is the WIP frame that the GPU draws to 34 | frame: [Colour; SCREEN_BUFFER_SIZE], 35 | // This is the last rendered frame displayed on the LCD, only updated 36 | // in VBlank. GUI implementations can read it to show the display. 37 | pub finished_frame: [Colour; SCREEN_BUFFER_SIZE], 38 | 39 | // X and Y of background position 40 | scy: u8, 41 | scx: u8, 42 | 43 | // X and Y of the Window 44 | wy: u8, 45 | wx: u8, 46 | 47 | // The scan-line Y co-ordinate 48 | ly: u8, 49 | // If ly is lyc ("compare") and the interrupt is enabled, 50 | // an LCD Status interrupt is flagged 51 | lyc: u8, 52 | // The "Window internal line counter" - relied upon by a handful of 53 | // unusual games and DMG-ACID2. 54 | window_line_counter: u8, 55 | 56 | // Scan-line X co-ordinate 57 | // This isn't a real readable Gameboy address, it's just for internal tracking 58 | lx: u16, 59 | 60 | bg_pallette: u8, 61 | sprite_pallete_1: u8, 62 | sprite_pallete_2: u8, 63 | 64 | status: LcdStatus, 65 | control: LcdControl, 66 | 67 | // "Object Attribute Memory" - Sprite properties 68 | oam: Ram, 69 | 70 | dma_source: u8, 71 | dma_cycles: u8, 72 | 73 | cgb_dma: CgbDmaConfig, 74 | 75 | // The global 40-sprite OAM cache 76 | // SmallVec doesn't do blocks of 40 so we leave 24 empty slots, it's still 77 | // more performant than allocating. 78 | sprite_cache: SmallVec<[Sprite; 64]>, 79 | // The per-scanline 10-sprite cache 80 | // TODO: These come straight from sprite_cache. Maybe they can be &Sprite? 81 | // Would that be faster? 82 | sprites_on_line: SmallVec<[Sprite; 10]>, 83 | } 84 | 85 | impl Gpu { 86 | // Function complexity warning here is due to the massive switch statement. 87 | // Such a thing is expected in an emulator. 88 | // skipcq: RS-R1000 89 | pub fn raw_write( 90 | &mut self, 91 | raw_address: u16, 92 | value: u8, 93 | ints: &mut Interrupts, 94 | ) { 95 | match raw_address { 96 | OAM_START..=OAM_END => { 97 | self.oam.write(raw_address - OAM_START, value) 98 | }, 99 | 100 | 0xFF40 => { 101 | let original_display_enable = self.control.display_enable; 102 | self.control = LcdControl::from(value); 103 | 104 | if original_display_enable && !self.control.display_enable { 105 | // The LCD has just been turned off 106 | self.ly = 0; 107 | self.status.set_mode(LcdMode::HBlank); 108 | if self.status.hblank_interrupt { 109 | ints.raise_interrupt(InterruptReason::LCDStat) 110 | } 111 | self.lyc = 0; 112 | } 113 | if !original_display_enable && self.control.display_enable { 114 | // The LCD has just been turned on 115 | self.status.set_mode(LcdMode::OAMSearch); 116 | if self.status.oam_interrupt { 117 | ints.raise_interrupt(InterruptReason::LCDStat); 118 | } 119 | self.cache_all_sprites(); 120 | } 121 | }, 122 | 0xFF41 => self.status.set_data(value, ints), 123 | 0xFF42 => self.scy = value, 124 | 0xFF43 => self.scx = value, 125 | // The Y Scanline is read only. 126 | // Space Invaders writes here. As a bug? 127 | 0xFF44 => {}, 128 | 0xFF45 => self.lyc = value, 129 | 130 | 0xFF46 => self.begin_dma(value), 131 | 132 | 0xFF47 => self.bg_pallette = value, 133 | 0xFF48 => self.sprite_pallete_1 = value, 134 | 0xFF49 => self.sprite_pallete_2 = value, 135 | 136 | 0xFF4A => self.wy = value, 137 | 0xFF4B => self.wx = value, 138 | 139 | 0xFF4C => log!( 140 | "[WARN] Unknown LCD register write at {:#06x} (value: {:#04x})", 141 | raw_address, 142 | value 143 | ), 144 | 145 | 0xFF51 => self.cgb_dma.set_source_upper(value), 146 | 0xFF52 => self.cgb_dma.set_source_lower(value), 147 | 0xFF53 => self.cgb_dma.set_dest_upper(value), 148 | 0xFF54 => self.cgb_dma.set_dest_lower(value), 149 | 0xFF55 => self.cgb_dma.set_config_byte(value), 150 | 151 | _ => panic!( 152 | "Unsupported GPU write at {:#06x} (value: {:#04x})", 153 | raw_address, value 154 | ), 155 | } 156 | } 157 | pub fn raw_read(&self, raw_address: u16) -> u8 { 158 | match raw_address { 159 | OAM_START..=OAM_END => self.oam.read(raw_address - OAM_START), 160 | 161 | 0xFF40 => u8::from(self.control), 162 | 0xFF41 => u8::from(self.status), 163 | 0xFF42 => self.scy, 164 | 0xFF43 => self.scx, 165 | 0xFF44 => self.ly, 166 | 0xFF45 => self.lyc, 167 | 168 | 0xFF46 => self.dma_source, 169 | 170 | 0xFF4A => self.wy, 171 | 0xFF4B => self.wx, 172 | 173 | 0xFF47 => self.bg_pallette, 174 | 0xFF48 => self.sprite_pallete_1, 175 | 0xFF49 => self.sprite_pallete_2, 176 | 177 | // High and low bits of a 16-bit register 178 | 0xFF51 => self.cgb_dma.get_source_upper(), 179 | 0xFF52 => self.cgb_dma.get_source_lower(), 180 | 0xFF53 => self.cgb_dma.get_dest_upper(), 181 | 0xFF54 => self.cgb_dma.get_dest_lower(), 182 | 0xFF55 => self.cgb_dma.get_config_byte(), 183 | 184 | _ => { 185 | log!("Unsupported GPU read at {:#06x}", raw_address); 186 | 0xFF 187 | }, 188 | } 189 | } 190 | 191 | fn cache_all_sprites(&mut self) { 192 | // There's room for 40 sprites in the OAM table 193 | let mut i = 0; 194 | while i < 40 { 195 | let address: u16 = i as u16 * 4; 196 | 197 | let y_pos = self.oam.read(address) as i32 - 16; 198 | let x_pos = self.oam.read(address + 1) as i32 - 8; 199 | let pattern_id = self.oam.read(address + 2); 200 | let attribs = self.oam.read(address + 3); 201 | 202 | let above_bg = (attribs & 0b1000_0000) == 0; 203 | let y_flip = (attribs & 0b0100_0000) > 0; 204 | let x_flip = (attribs & 0b0010_0000) > 0; 205 | let use_palette_0 = (attribs & 0b0001_0000) == 0; 206 | let use_upper_vram_bank = (attribs & 0b0000_1000) > 0; 207 | let cgb_palette = attribs & 0b0000_0111; 208 | 209 | if self.sprite_cache.len() > i { 210 | self.sprite_cache[i] = Sprite { 211 | y_pos, 212 | x_pos, 213 | pattern_id, 214 | above_bg, 215 | y_flip, 216 | x_flip, 217 | use_palette_0, 218 | use_upper_vram_bank, 219 | cgb_palette, 220 | }; 221 | } else { 222 | self.sprite_cache.push(Sprite { 223 | y_pos, 224 | x_pos, 225 | pattern_id, 226 | above_bg, 227 | y_flip, 228 | x_flip, 229 | use_palette_0, 230 | use_upper_vram_bank, 231 | cgb_palette, 232 | }); 233 | } 234 | 235 | i += 1; 236 | } 237 | 238 | self.sprite_cache.truncate(i); 239 | } 240 | 241 | fn begin_dma(&mut self, source: u8) { 242 | // Really, we should be disabling access to anything but HRAM now, 243 | // but if the rom is nice then there shouldn't be an issue. 244 | if self.dma_cycles != 0 { 245 | log!("INTERRUPTING DMA!") 246 | } 247 | self.dma_source = source; 248 | self.dma_cycles = gpu_timing::DMA_CYCLES; 249 | } 250 | 251 | fn update_cgb_generic_dma( 252 | &mut self, 253 | ints: &mut Interrupts, 254 | mem: &mut Memory, 255 | ) { 256 | if self.cgb_dma.bytes_left > 0 && !self.cgb_dma.is_hblank_dma() { 257 | for i in 0..self.cgb_dma.bytes_left { 258 | let value = mem.read(ints, self, self.cgb_dma.source + i); 259 | mem.write(ints, self, self.cgb_dma.dest + i, value); 260 | } 261 | self.cgb_dma.bytes_left = 0; 262 | } 263 | } 264 | 265 | fn update_cgb_hblank_dma( 266 | &mut self, 267 | ints: &mut Interrupts, 268 | mem: &mut Memory, 269 | ) { 270 | if self.cgb_dma.bytes_left > 0 && self.cgb_dma.is_hblank_dma() { 271 | for i in 0..0x10 { 272 | // TODO: This shouldn't be called, all DMAs are a multiple of 0x10 long 273 | if self.cgb_dma.bytes_left == 0 { 274 | break; 275 | } 276 | let value = mem.read( 277 | ints, 278 | self, 279 | self.cgb_dma.source + self.cgb_dma.bytes_copied + i, 280 | ); 281 | mem.write( 282 | ints, 283 | self, 284 | self.cgb_dma.dest + self.cgb_dma.bytes_copied + i, 285 | value, 286 | ); 287 | } 288 | self.cgb_dma.bytes_left -= 0x10; 289 | self.cgb_dma.bytes_copied += 0x10; 290 | } 291 | } 292 | 293 | fn update_dma(&mut self, ints: &mut Interrupts, mem: &mut Memory) { 294 | if self.cgb_features { 295 | self.update_cgb_generic_dma(ints, mem) 296 | } 297 | 298 | // There isn't one pending 299 | if self.dma_cycles == 0 { 300 | return; 301 | } 302 | 303 | self.dma_cycles -= 1; 304 | // Ready to actually perform DMA? 305 | if self.dma_cycles == 0 { 306 | let source = (self.dma_source as u16) * 0x100; 307 | 308 | for i in 0x00..=0x9F { 309 | let data = mem.read(ints, self, source + i); 310 | self.oam.write(i, data); 311 | } 312 | } 313 | } 314 | 315 | fn enter_vblank(&mut self, ints: &mut Interrupts) { 316 | ints.raise_interrupt(InterruptReason::VBlank); 317 | 318 | // TODO: This seems like odd behaviour to me. 319 | if self.status.vblank_interrupt { 320 | ints.raise_interrupt(InterruptReason::LCDStat); 321 | } 322 | 323 | self.finished_frame.clone_from(&self.frame); 324 | } 325 | 326 | fn run_ly_compare(&mut self, ints: &mut Interrupts) { 327 | if self.ly == self.lyc { 328 | self.status.coincidence_flag = true; 329 | 330 | if self.status.lyc { 331 | ints.raise_interrupt(InterruptReason::LCDStat); 332 | } 333 | } 334 | } 335 | 336 | pub fn step(&mut self, ints: &mut Interrupts, mem: &mut Memory) { 337 | // TODO: Check that a DMA is performed even with display off 338 | self.update_dma(ints, mem); 339 | 340 | if !self.control.display_enable { 341 | return; 342 | } 343 | 344 | self.lx += 1; 345 | if self.lx == gpu_timing::HTOTAL { 346 | self.lx = 0; 347 | } 348 | 349 | let mode = self.status.get_mode(); 350 | 351 | if mode == LcdMode::VBlank { 352 | if self.lx == 0 { 353 | self.ly += 1; 354 | if self.ly == gpu_timing::VTOTAL { 355 | self.ly = 0; 356 | } 357 | 358 | self.run_ly_compare(ints); 359 | 360 | if self.ly == 0 { 361 | self.window_line_counter = 0; 362 | if self.status.oam_interrupt { 363 | ints.raise_interrupt(InterruptReason::LCDStat); 364 | } 365 | self.status.set_mode(LcdMode::OAMSearch); 366 | self.cache_all_sprites(); 367 | self.draw_line_if_necessary(ints, mem); 368 | } 369 | } 370 | return; 371 | } 372 | 373 | if self.lx == 0 { 374 | // Unusual GPU implementation detail. This is only 375 | // incremented when the Window was drawn on this scanline. 376 | // TODO: Relate these magic numbers to constants. 377 | if self.control.window_enable 378 | && self.wx < 166 379 | && self.wy < 143 380 | && self.ly >= self.wy 381 | { 382 | self.window_line_counter += 1; 383 | } 384 | 385 | self.ly += 1; 386 | 387 | self.run_ly_compare(ints); 388 | // Done with frame, enter VBlank 389 | if self.ly == gpu_timing::VBLANK_ON { 390 | self.enter_vblank(ints); 391 | self.status.set_mode(LcdMode::VBlank); 392 | } else { 393 | if mode != LcdMode::OAMSearch { 394 | self.status.set_mode(LcdMode::OAMSearch); 395 | self.cache_all_sprites(); 396 | self.draw_line_if_necessary(ints, mem); 397 | } 398 | } 399 | return; 400 | } 401 | 402 | if self.lx == gpu_timing::HTRANSFER_ON { 403 | self.status.set_mode(LcdMode::Transfer); 404 | self.draw_line_if_necessary(ints, mem); 405 | return; 406 | } 407 | 408 | if self.lx == gpu_timing::HBLANK_ON { 409 | self.update_cgb_hblank_dma(ints, mem); 410 | if self.status.hblank_interrupt { 411 | ints.raise_interrupt(InterruptReason::LCDStat) 412 | } 413 | self.status.set_mode(LcdMode::HBlank); 414 | return; 415 | } 416 | 417 | self.draw_line_if_necessary(ints, mem); 418 | } 419 | 420 | #[inline(always)] 421 | fn draw_line_if_necessary( 422 | &mut self, 423 | ints: &mut Interrupts, 424 | mem: &mut Memory, 425 | ) { 426 | let line_start = 427 | gpu_timing::HTRANSFER_ON + if self.ly == 0 { 160 } else { 48 }; 428 | 429 | if self.lx == line_start { 430 | // Draw the current line 431 | // TODO: Move these draw_pixel calls into the mode switch 432 | // to allow mid-scanline visual effects 433 | self.cache_sprites_on_line(self.ly); 434 | for x in 0..(SCREEN_WIDTH as u8) { 435 | self.draw_pixel(ints, mem, x, self.ly); 436 | } 437 | } 438 | } 439 | 440 | fn draw_pixel(&mut self, ints: &Interrupts, mem: &Memory, x: u8, y: u8) { 441 | let ux = x as usize; 442 | let uy = y as usize; 443 | let idx = uy * SCREEN_WIDTH + ux; 444 | 445 | let bg_col: Colour; 446 | let bg_col_id = if self.cgb_features || self.control.bg_display { 447 | let (new_col, id) = self.get_background_colour_at(ints, mem, x, y); 448 | bg_col = new_col; 449 | id 450 | } else { 451 | bg_col = grey_shades::white(); 452 | 0 453 | }; 454 | 455 | // If there's a non-transparent sprite here, use its colour 456 | let s_col = self.get_sprite_colour_at(mem, bg_col, bg_col_id, x, y); 457 | 458 | self.frame[idx] = s_col; 459 | } 460 | 461 | fn get_colour_id_in_line(&self, tile_line: u16, subx: u8) -> u16 { 462 | let lower = tile_line & 0xFF; 463 | let upper = (tile_line & 0xFF00) >> 8; 464 | 465 | let shift_amnt = 7 - subx; 466 | let mask = 1 << shift_amnt; 467 | let u = (upper & mask) >> shift_amnt; 468 | let l = (lower & mask) >> shift_amnt; 469 | let pixel_colour_id = (u << 1) | l; 470 | 471 | pixel_colour_id 472 | } 473 | 474 | fn get_shade_from_colour_id( 475 | &self, 476 | pixel_colour_id: u16, 477 | palette: u8, 478 | ) -> Colour { 479 | let shift_2 = pixel_colour_id * 2; 480 | let shade = (palette & (0b11 << shift_2)) >> shift_2; 481 | 482 | colour_from_grey_shade_id(shade) 483 | } 484 | 485 | fn get_background_colour_at( 486 | &self, 487 | ints: &Interrupts, 488 | mem: &Memory, 489 | x: u8, 490 | y: u8, 491 | ) -> (Colour, u16) { 492 | let is_window = self.control.window_enable 493 | && x as isize > self.wx as isize - 8 494 | && y >= self.wy; 495 | 496 | let tilemap_select = if is_window { 497 | self.control.window_tile_map_display_select 498 | } else { 499 | self.control.bg_tile_map_display_select 500 | }; 501 | 502 | let tilemap_base = if tilemap_select { 0x9C00 } else { 0x9800 }; 503 | 504 | // This is which tile ID our pixel is in 505 | let x16: u16; 506 | let y16: u16; 507 | 508 | if is_window { 509 | // TODO: Check this saturating_sub, it's a guess. 510 | // Super Mario Bros Deluxe pause menu crashes without it 511 | x16 = x.wrapping_sub(self.wx.saturating_sub(7)) as u16; 512 | y16 = self.window_line_counter as u16; 513 | } else { 514 | x16 = x.wrapping_add(self.scx) as u16; 515 | y16 = y.wrapping_add(self.scy) as u16; 516 | } 517 | 518 | let tx = x16 / 8; 519 | let ty = y16 / 8; 520 | // NOTE: Things like y16 % 8 is equivalent to y16 - ty * 8 521 | // However, this is not more performant. I think the compiler 522 | // is smart enough to recognise that. 523 | let mut subx = (x16 % 8) as u8; 524 | let mut suby = y16 % 8; 525 | 526 | let byte_offset = ty * 32 + tx; 527 | let tilemap_address = tilemap_base + byte_offset; 528 | let tile_metadata = mem.vram.bg_map_attributes.get_entry(byte_offset); 529 | 530 | let tile_id_raw = mem.read(ints, self, tilemap_address); 531 | let tile_id: u16; 532 | 533 | if self.control.bg_and_window_data_select { 534 | // 0x8000 addressing mode 535 | tile_id = tile_id_raw as u16; 536 | } else { 537 | // 0x8800 addressing mode 538 | if tile_id_raw < 128 { 539 | tile_id = (tile_id_raw as u16) + 256; 540 | } else { 541 | tile_id = tile_id_raw as u16 542 | } 543 | } 544 | 545 | // BG tile flipping is a CGB-exclusive feature 546 | if self.cgb_features { 547 | if tile_metadata.x_flip { 548 | subx = 7 - subx; 549 | } 550 | if tile_metadata.y_flip { 551 | suby = 7 - suby; 552 | } 553 | } 554 | 555 | let tile_byte_offset = tile_id * 16; 556 | let tile_line_offset = tile_byte_offset + (suby * 2); 557 | 558 | // This is the line of the tile data that out pixel resides on 559 | let tiledata_base = 0x8000; 560 | let tile_address = tiledata_base + tile_line_offset; 561 | 562 | let bank = if self.cgb_features { 563 | tile_metadata.vram_bank as u16 564 | } else { 565 | 0 566 | }; 567 | let tile_line0 = mem.vram.read_arbitrary_bank(bank, tile_address); 568 | let tile_line1 = mem.vram.read_arbitrary_bank(bank, tile_address + 1); 569 | let tile_line = combine_u8!(tile_line1, tile_line0); 570 | 571 | let col_id = self.get_colour_id_in_line(tile_line, subx); 572 | 573 | if self.cgb_features { 574 | let colour = mem 575 | .palette_ram 576 | .get_bg_palette_colour(tile_metadata.palette as u16, col_id); 577 | (colour, col_id) 578 | } else { 579 | ( 580 | self.get_shade_from_colour_id(col_id, self.bg_pallette), 581 | col_id, 582 | ) 583 | } 584 | } 585 | 586 | fn get_sprite_colour_at( 587 | &self, 588 | mem: &Memory, 589 | bg_col: Colour, 590 | bg_col_id: u16, 591 | x: u8, 592 | y: u8, 593 | ) -> Colour { 594 | // Sprites are hidden for this scanline 595 | if !self.control.obj_enable { 596 | return bg_col; 597 | } 598 | 599 | let sprite_height = if self.control.obj_size { 16 } else { 8 }; 600 | 601 | let ix = x as i32; 602 | let iy = y as i32; 603 | 604 | let mut maybe_colour: Option = None; 605 | let mut min_x: i32 = SCREEN_WIDTH as i32 + 8; 606 | for sprite in &self.sprites_on_line { 607 | let mut above_bg = sprite.above_bg; 608 | // In CGB mode, bg_display off means sprites always get priority. 609 | if self.cgb_features && !self.control.bg_display { 610 | above_bg = true; 611 | } 612 | 613 | if sprite.x_pos <= ix 614 | && (sprite.x_pos + 8) > ix 615 | && sprite.x_pos < min_x 616 | { 617 | if !above_bg && bg_col_id != 0 { 618 | continue; 619 | } 620 | 621 | let mut subx = (ix - sprite.x_pos) as u8; 622 | let mut suby = iy - sprite.y_pos; 623 | 624 | // Tile address for 8x8 mode 625 | let mut pattern = sprite.pattern_id; 626 | 627 | if sprite_height == 16 { 628 | if suby > 7 { 629 | suby -= 8; 630 | 631 | if sprite.y_flip { 632 | pattern = sprite.pattern_id & 0xFE; 633 | } else { 634 | pattern = sprite.pattern_id | 0x01; 635 | } 636 | } else { 637 | if sprite.y_flip { 638 | pattern = sprite.pattern_id | 0x01; 639 | } else { 640 | pattern = sprite.pattern_id & 0xFE; 641 | } 642 | } 643 | } 644 | 645 | if sprite.x_flip { 646 | subx = 7 - subx 647 | } 648 | // TODO: Not sure if this applies to vertically flipped 8x16 mode sprites 649 | if sprite.y_flip { 650 | suby = 7 - suby 651 | } 652 | 653 | let tile_address = 0x8000 + (pattern as u16) * 16; 654 | let line_we_need = suby as u16 * 2; 655 | let bank = if self.cgb_features && sprite.use_upper_vram_bank { 656 | 1 657 | } else { 658 | 0 659 | }; 660 | let tile_address = tile_address + line_we_need; 661 | // log!("Sprite at [{},{}] is using upper VRAM bank? {:?}, pattern_id {}, address: {:#06x}", sprite.x_pos, sprite.y_pos, sprite.use_upper_vram_bank, sprite.pattern_id, tile_address); 662 | 663 | let tile_line0 = 664 | mem.vram.read_arbitrary_bank(bank, tile_address); 665 | let tile_line1 = 666 | mem.vram.read_arbitrary_bank(bank, tile_address + 1); 667 | let tile_line = combine_u8!(tile_line1, tile_line0); 668 | 669 | let col_id = self.get_colour_id_in_line(tile_line, subx); 670 | 671 | if col_id == 0 { 672 | // This pixel is transparent 673 | continue; 674 | } else { 675 | if self.cgb_features { 676 | let colour = mem.palette_ram.get_obj_palette_colour( 677 | sprite.cgb_palette as u16, 678 | col_id, 679 | ); 680 | maybe_colour = Some(colour) 681 | } else { 682 | let palette = if sprite.use_palette_0 { 683 | self.sprite_pallete_1 684 | } else { 685 | self.sprite_pallete_2 686 | }; 687 | 688 | min_x = sprite.x_pos; 689 | maybe_colour = 690 | Some(self.get_shade_from_colour_id(col_id, palette)) 691 | } 692 | } 693 | } 694 | } 695 | 696 | match maybe_colour { 697 | Some(col) => col, 698 | None => bg_col, 699 | } 700 | } 701 | 702 | // Will be used later for get_sprite_pixel 703 | fn cache_sprites_on_line(&mut self, y: u8) { 704 | let sprite_height = if self.control.obj_size { 16 } else { 8 }; 705 | 706 | let iy = y as i32; 707 | self.sprites_on_line.truncate(0); 708 | for s in &self.sprite_cache { 709 | if s.y_pos <= iy && (s.y_pos + sprite_height) > iy { 710 | self.sprites_on_line.push(s.clone()); 711 | } 712 | if self.sprites_on_line.len() == 10 { 713 | break; 714 | } 715 | } 716 | } 717 | 718 | pub fn get_rgba_frame(&self) -> [u8; SCREEN_RGBA_SLICE_SIZE] { 719 | let mut out_array = [0; SCREEN_RGBA_SLICE_SIZE]; 720 | for i in 0..SCREEN_BUFFER_SIZE { 721 | let start = i * 4; 722 | out_array[start] = self.finished_frame[i].red; 723 | out_array[start + 1] = self.finished_frame[i].green; 724 | out_array[start + 2] = self.finished_frame[i].blue; 725 | out_array[start + 3] = 0xFF; 726 | } 727 | out_array 728 | } 729 | 730 | pub fn new(cgb_features: bool) -> Gpu { 731 | let empty_frame = [grey_shades::white(); SCREEN_BUFFER_SIZE]; 732 | Gpu { 733 | cgb_features, 734 | frame: empty_frame, 735 | finished_frame: empty_frame.clone(), 736 | window_line_counter: 0, 737 | scy: 0, 738 | scx: 0, 739 | ly: 0, 740 | lx: 0, 741 | lyc: 0, 742 | wy: 0, 743 | wx: 0, 744 | bg_pallette: 0, 745 | sprite_pallete_1: 0, 746 | sprite_pallete_2: 0, 747 | status: LcdStatus::new(), 748 | control: LcdControl::new(), 749 | oam: Ram::new(OAM_SIZE), 750 | dma_source: 0, 751 | dma_cycles: 0, 752 | cgb_dma: CgbDmaConfig::new(), 753 | sprite_cache: SmallVec::with_capacity(40), 754 | sprites_on_line: SmallVec::with_capacity(10), 755 | } 756 | } 757 | } 758 | -------------------------------------------------------------------------------- /core/src/helpers.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! combine_u8( 3 | ($x:expr, $y:expr) => ( 4 | (($x as u16) << 8) | $y as u16 5 | ) 6 | ); 7 | #[macro_export] 8 | macro_rules! split_u16( 9 | ($n:expr) => ({ 10 | let b1 = ($n & 0x00FF) as u8; 11 | let b2 = (($n & 0xFF00) >> 8) as u8; 12 | (b1, b2) 13 | }) 14 | ); 15 | #[macro_export] 16 | macro_rules! set_bit( 17 | ($number:expr, $bit_index:expr, $bit:expr) => ( 18 | $number = ($number & !(1 << $bit_index)) | $bit << $bit_index; 19 | ) 20 | ); 21 | 22 | #[macro_export] 23 | macro_rules! log { 24 | ($($a:expr),*) => { 25 | { 26 | #[cfg(not(feature = "std"))] 27 | use alloc::format; 28 | ($crate::callbacks::CALLBACKS.lock().log)(&format!($($a,)*)[..]) 29 | } 30 | }; 31 | } 32 | 33 | // Macro for bit-matching 34 | // https://www.reddit.com/r/rust/comments/2d7rrj/comment/cjo2c7t/?context=3 35 | #[macro_export] 36 | macro_rules! compute_mask { 37 | (0) => { 38 | 1 39 | }; 40 | (1) => { 41 | 1 42 | }; 43 | (_) => { 44 | 0 45 | }; 46 | } 47 | #[macro_export] 48 | macro_rules! compute_equal { 49 | (0) => { 50 | 0 51 | }; 52 | (1) => { 53 | 1 54 | }; 55 | (_) => { 56 | 0 57 | }; 58 | } 59 | #[macro_export] 60 | macro_rules! bitmatch( 61 | ($x: expr, ($($b: tt),*)) => ({ 62 | let mut mask = 0; 63 | let mut val = 0; 64 | $( 65 | mask = (mask << 1) | compute_mask!($b); 66 | val = (val << 1) | compute_equal!($b); 67 | )* 68 | ($x & mask) == val 69 | }); 70 | ); 71 | -------------------------------------------------------------------------------- /core/src/interrupts.rs: -------------------------------------------------------------------------------- 1 | pub const INTERRUPT_VECTORS: [u16; 5] = [0x40, 0x48, 0x50, 0x58, 0x60]; 2 | 3 | #[derive(Clone)] 4 | pub struct InterruptFields { 5 | pub v_blank: bool, 6 | pub lcd_stat: bool, 7 | pub timer: bool, 8 | pub serial: bool, 9 | pub joypad: bool, 10 | } 11 | 12 | pub enum InterruptReason { 13 | VBlank, 14 | LCDStat, 15 | Timer, 16 | Serial, 17 | Joypad, 18 | } 19 | fn get_interrupt_reason_bitmask(reason: InterruptReason) -> u8 { 20 | match reason { 21 | InterruptReason::VBlank => 0b00000001, 22 | InterruptReason::LCDStat => 0b00000010, 23 | InterruptReason::Timer => 0b00000100, 24 | InterruptReason::Serial => 0b00001000, 25 | InterruptReason::Joypad => 0b00010000, 26 | } 27 | } 28 | 29 | impl InterruptFields { 30 | // TODO: Check if these actually do all start false 31 | pub fn new() -> InterruptFields { 32 | InterruptFields { 33 | v_blank: false, 34 | lcd_stat: false, 35 | timer: false, 36 | serial: false, 37 | joypad: false, 38 | } 39 | } 40 | } 41 | impl From for InterruptFields { 42 | fn from(n: u8) -> InterruptFields { 43 | InterruptFields { 44 | v_blank: (n & 1) == 1, 45 | lcd_stat: ((n >> 1) & 1) == 1, 46 | timer: ((n >> 2) & 1) == 1, 47 | serial: ((n >> 3) & 1) == 1, 48 | joypad: ((n >> 4) & 1) == 1, 49 | } 50 | } 51 | } 52 | impl From for u8 { 53 | fn from(f: InterruptFields) -> u8 { 54 | let b1 = f.v_blank as u8; 55 | let b2 = (f.lcd_stat as u8) << 1; 56 | let b3 = (f.timer as u8) << 2; 57 | let b4 = (f.serial as u8) << 3; 58 | let b5 = (f.joypad as u8) << 4; 59 | b1 | b2 | b3 | b4 | b5 60 | } 61 | } 62 | 63 | pub struct Interrupts { 64 | pub enable: InterruptFields, 65 | pub flag: InterruptFields, 66 | 67 | // "Interrupts master enabled" flag 68 | pub ime: bool, 69 | } 70 | 71 | impl Interrupts { 72 | #[inline(always)] 73 | pub fn raise_interrupt(&mut self, reason: InterruptReason) { 74 | let mut data = self.flag_read(); 75 | data |= get_interrupt_reason_bitmask(reason); 76 | self.flag_write(data); 77 | } 78 | 79 | // Called when GB writes to FFFF 80 | #[inline(always)] 81 | pub fn enable_write(&mut self, value: u8) { 82 | // log!("{:08b} written to IE", value); 83 | self.enable = InterruptFields::from(value) 84 | } 85 | 86 | // Called when GB writes to FF0F 87 | #[inline(always)] 88 | pub fn flag_write(&mut self, value: u8) { 89 | self.flag = InterruptFields::from(value) 90 | } 91 | 92 | // Called when GB reads from FFFF 93 | #[inline(always)] 94 | pub fn enable_read(&self) -> u8 { 95 | u8::from(self.enable.clone()) 96 | } 97 | 98 | // Called when GB reads from FF0F 99 | #[inline(always)] 100 | pub fn flag_read(&self) -> u8 { 101 | u8::from(self.flag.clone()) 102 | } 103 | 104 | pub fn new() -> Interrupts { 105 | Interrupts { 106 | enable: InterruptFields::new(), 107 | flag: InterruptFields::new(), 108 | 109 | ime: false, 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/joypad.rs: -------------------------------------------------------------------------------- 1 | enum JoypadReadoutMode { 2 | Buttons, 3 | Directions, 4 | Neither, 5 | } 6 | 7 | // TODO: Raise the Joypad interrupt 8 | pub struct Joypad { 9 | readout_mode: JoypadReadoutMode, 10 | 11 | // The GUI writes these values directly via the keyboard 12 | // Every frame. 13 | pub up_pressed: bool, 14 | pub down_pressed: bool, 15 | pub left_pressed: bool, 16 | pub right_pressed: bool, 17 | pub a_pressed: bool, 18 | pub b_pressed: bool, 19 | pub start_pressed: bool, 20 | pub select_pressed: bool, 21 | } 22 | 23 | impl Joypad { 24 | #[inline(always)] 25 | pub fn write(&mut self, n: u8) { 26 | let masked = n & 0b0011_0000; 27 | 28 | self.readout_mode = match masked { 29 | 0b0001_0000 => JoypadReadoutMode::Buttons, 30 | 0b0010_0000 => JoypadReadoutMode::Directions, 31 | _ => JoypadReadoutMode::Neither, 32 | } 33 | } 34 | 35 | #[inline(always)] 36 | fn direction_bits(&self) -> u8 { 37 | (!self.right_pressed as u8) 38 | | ((!self.left_pressed as u8) << 1) 39 | | ((!self.up_pressed as u8) << 2) 40 | | ((!self.down_pressed as u8) << 3) 41 | } 42 | 43 | #[inline(always)] 44 | fn button_bits(&self) -> u8 { 45 | (!self.a_pressed as u8) 46 | | ((!self.b_pressed as u8) << 1) 47 | | ((!self.select_pressed as u8) << 2) 48 | | ((!self.start_pressed as u8) << 3) 49 | } 50 | 51 | #[inline(always)] 52 | fn selection_bits(&self) -> u8 { 53 | match self.readout_mode { 54 | JoypadReadoutMode::Buttons => 0b0010_0000, 55 | JoypadReadoutMode::Directions => 0b0001_0000, 56 | JoypadReadoutMode::Neither => 0, 57 | } 58 | } 59 | 60 | #[inline(always)] 61 | pub fn read(&self) -> u8 { 62 | let n = match self.readout_mode { 63 | JoypadReadoutMode::Buttons => self.button_bits(), 64 | JoypadReadoutMode::Directions => self.direction_bits(), 65 | JoypadReadoutMode::Neither => 0xF, 66 | }; 67 | 68 | n | self.selection_bits() 69 | } 70 | 71 | pub fn new() -> Joypad { 72 | Joypad { 73 | readout_mode: JoypadReadoutMode::Buttons, 74 | up_pressed: false, 75 | down_pressed: false, 76 | left_pressed: false, 77 | right_pressed: false, 78 | a_pressed: false, 79 | b_pressed: false, 80 | start_pressed: false, 81 | select_pressed: false, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/lcd.rs: -------------------------------------------------------------------------------- 1 | use crate::interrupts::{InterruptReason, Interrupts}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct LcdControl { 5 | pub display_enable: bool, 6 | pub window_tile_map_display_select: bool, 7 | pub window_enable: bool, 8 | pub bg_and_window_data_select: bool, 9 | pub bg_tile_map_display_select: bool, 10 | pub obj_size: bool, 11 | pub obj_enable: bool, 12 | pub bg_display: bool, 13 | } 14 | impl LcdControl { 15 | pub fn new() -> LcdControl { 16 | // This value is set by the DMG boot rom. 17 | LcdControl::from(0b10000101) 18 | } 19 | } 20 | impl From for LcdControl { 21 | fn from(n: u8) -> LcdControl { 22 | LcdControl { 23 | bg_display: (n & 0b0000_0001) == 0b0000_0001, 24 | obj_enable: (n & 0b0000_0010) == 0b0000_0010, 25 | obj_size: (n & 0b0000_0100) == 0b0000_0100, 26 | bg_tile_map_display_select: (n & 0b0000_1000) == 0b0000_1000, 27 | bg_and_window_data_select: (n & 0b0001_0000) == 0b0001_0000, 28 | window_enable: (n & 0b0010_0000) == 0b0010_0000, 29 | window_tile_map_display_select: (n & 0b0100_0000) == 0b0100_0000, 30 | display_enable: (n & 0b1000_0000) == 0b1000_0000, 31 | } 32 | } 33 | } 34 | impl From for u8 { 35 | fn from(lcd: LcdControl) -> u8 { 36 | lcd.bg_display as u8 37 | | (lcd.obj_enable as u8) << 1 38 | | (lcd.obj_size as u8) << 2 39 | | (lcd.bg_tile_map_display_select as u8) << 3 40 | | (lcd.bg_and_window_data_select as u8) << 4 41 | | (lcd.window_enable as u8) << 5 42 | | (lcd.window_tile_map_display_select as u8) << 6 43 | | (lcd.display_enable as u8) << 7 44 | } 45 | } 46 | 47 | #[derive(PartialEq, Clone, Debug)] 48 | pub enum LcdMode { 49 | HBlank = 0, 50 | VBlank = 1, 51 | OAMSearch = 2, 52 | Transfer = 3, 53 | } 54 | 55 | #[derive(Clone, Copy)] 56 | pub struct LcdStatus { 57 | pub lyc: bool, 58 | pub oam_interrupt: bool, 59 | pub vblank_interrupt: bool, 60 | pub hblank_interrupt: bool, 61 | pub coincidence_flag: bool, 62 | pub mode_flag: u8, 63 | } 64 | impl LcdStatus { 65 | pub fn get_mode(&self) -> LcdMode { 66 | match self.mode_flag { 67 | 0 => LcdMode::HBlank, 68 | 1 => LcdMode::VBlank, 69 | 2 => LcdMode::OAMSearch, 70 | 3 => LcdMode::Transfer, 71 | _ => panic!("Invalid LCD mode"), 72 | } 73 | } 74 | 75 | #[inline(always)] 76 | pub fn set_data(&mut self, data: u8, ints: &mut Interrupts) { 77 | // There's actually a DMG GPU bug when writing to LCDStat 78 | // where sometimes it fires off an interrupt at the wrong time 79 | // https://robertovaccari.com/blog/2020_09_26_gameboy/ 80 | match self.get_mode() { 81 | LcdMode::HBlank | LcdMode::VBlank => { 82 | if self.lyc { 83 | ints.raise_interrupt(InterruptReason::LCDStat) 84 | } 85 | }, 86 | _ => {}, 87 | } 88 | 89 | let new_stat = LcdStatus::from(data); 90 | self.lyc = new_stat.lyc; 91 | self.oam_interrupt = new_stat.oam_interrupt; 92 | self.vblank_interrupt = new_stat.vblank_interrupt; 93 | self.hblank_interrupt = new_stat.hblank_interrupt; 94 | // NOTE: We *don't* set the coincidence_flag or mode_flag, 95 | // they're read only 96 | } 97 | 98 | #[inline(always)] 99 | pub fn set_mode(&mut self, mode: LcdMode) { 100 | self.mode_flag = mode as u8; 101 | } 102 | 103 | pub fn new() -> LcdStatus { 104 | // LCD starts in OAMSearch, not HBlank 105 | LcdStatus::from(0b000000_10) 106 | } 107 | } 108 | impl From for LcdStatus { 109 | #[inline(always)] 110 | fn from(n: u8) -> LcdStatus { 111 | LcdStatus { 112 | lyc: (n & 0b1000000) == 0b1000000, 113 | oam_interrupt: (n & 0b100000) == 0b100000, 114 | vblank_interrupt: (n & 0b10000) == 0b10000, 115 | hblank_interrupt: (n & 0b1000) == 0b1000, 116 | coincidence_flag: (n & 0b100) == 0b100, 117 | mode_flag: n & 0b11, 118 | } 119 | } 120 | } 121 | impl From for u8 { 122 | #[inline(always)] 123 | fn from(lcd: LcdStatus) -> u8 { 124 | lcd.mode_flag 125 | | (lcd.coincidence_flag as u8) << 2 126 | | (lcd.hblank_interrupt as u8) << 3 127 | | (lcd.vblank_interrupt as u8) << 4 128 | | (lcd.oam_interrupt as u8) << 5 129 | | (lcd.lyc as u8) << 6 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | #[cfg(not(feature = "std"))] 4 | extern crate alloc; 5 | 6 | pub mod alu; 7 | pub mod callbacks; 8 | pub mod cartridge; 9 | pub mod cgb_dma; 10 | pub mod colour; // innit bruv 11 | pub mod config; 12 | pub mod constants; 13 | pub mod cpu; 14 | pub mod gpu; 15 | pub mod helpers; 16 | pub mod interrupts; 17 | pub mod joypad; 18 | pub mod lcd; 19 | pub mod memory; 20 | pub mod registers; 21 | pub mod serial_cable; 22 | pub mod sound; 23 | -------------------------------------------------------------------------------- /core/src/memory/battery_backed_ram.rs: -------------------------------------------------------------------------------- 1 | // RAM with a save file 2 | use crate::{callbacks::CALLBACKS, cartridge::Cartridge, memory::ram::Ram}; 3 | 4 | // The amount of milliseconds we wait before saving our save file 5 | // (otherwise eg. Link's Awakening would write 2,700 save files 6 | // on its first frame) 7 | const DEBOUNCE_MILLIS: usize = 1000; 8 | 9 | pub struct BatteryBackedRam { 10 | pub ram: Ram, 11 | pub size: usize, 12 | 13 | cart: Cartridge, 14 | 15 | battery_enabled: bool, 16 | changed_since_last_save: bool, 17 | last_saved_at: usize, 18 | } 19 | 20 | impl BatteryBackedRam { 21 | pub fn read(&self, address: u16) -> u8 { 22 | self.ram.read(address) 23 | } 24 | 25 | pub fn read_usize(&self, address: usize) -> u8 { 26 | self.ram.bytes[address] 27 | } 28 | 29 | pub fn write(&mut self, address: u16, value: u8) { 30 | self.ram.write(address, value); 31 | self.changed_since_last_save = true; 32 | } 33 | 34 | pub fn write_usize(&mut self, address: usize, value: u8) { 35 | self.ram.bytes[address] = value; 36 | self.changed_since_last_save = true; 37 | } 38 | 39 | pub fn step(&mut self, ms_since_boot: usize) { 40 | if !self.changed_since_last_save || !self.battery_enabled { 41 | return; 42 | } 43 | 44 | let millis_since_last_save = ms_since_boot - self.last_saved_at; 45 | 46 | if millis_since_last_save >= DEBOUNCE_MILLIS { 47 | self.last_saved_at = ms_since_boot; 48 | self.save_ram_contents() 49 | } 50 | } 51 | 52 | fn save_ram_contents(&mut self) { 53 | self.changed_since_last_save = false; 54 | 55 | (CALLBACKS.lock().save)( 56 | &self.cart.title[..], 57 | &self.cart.rom_path[..], 58 | &self.ram.bytes, 59 | ); 60 | } 61 | 62 | pub fn new( 63 | cart: Cartridge, 64 | additional_ram_size: usize, 65 | battery_enabled: bool, 66 | ) -> BatteryBackedRam { 67 | // Some MBCs, like MBC2, always have a few bytes of RAM installed. 68 | // The cartridge header only tells us about additional external RAM. 69 | let ram_size = cart.ram_size + additional_ram_size; 70 | 71 | let save_contents = (CALLBACKS.lock().load)( 72 | &cart.title[..], 73 | &cart.rom_path[..], 74 | ram_size, 75 | ); 76 | 77 | let ram = Ram::from_bytes(save_contents, ram_size); 78 | 79 | BatteryBackedRam { 80 | ram, 81 | size: ram_size, 82 | 83 | cart, 84 | battery_enabled, 85 | changed_since_last_save: false, 86 | 87 | last_saved_at: 0, 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /core/src/memory/cgb_speed_switch.rs: -------------------------------------------------------------------------------- 1 | use crate::log; 2 | 3 | pub struct CgbSpeedSwitch { 4 | pub armed: bool, 5 | pub current_speed_is_double: bool, 6 | cgb_features: bool, 7 | } 8 | 9 | // TODO: Actually act on this for CPU speed. 10 | // This just tracks the byte state. 11 | impl CgbSpeedSwitch { 12 | pub fn write_switch_byte(&mut self, value: u8) { 13 | if self.cgb_features { 14 | self.armed = value & 1 > 0; 15 | } 16 | } 17 | pub fn read_switch_byte(&self) -> u8 { 18 | let top_bit = if self.current_speed_is_double { 19 | 0x80 20 | } else { 21 | 0x00 22 | }; 23 | let bottom_bit = if self.armed { 0x01 } else { 0x00 }; 24 | top_bit | bottom_bit 25 | } 26 | pub fn execute_speed_switch(&mut self) { 27 | self.armed = false; 28 | self.current_speed_is_double = !self.current_speed_is_double; 29 | log!( 30 | "Performing CGB speed switch. New speed: {}", 31 | match self.current_speed_is_double { 32 | true => "Double", 33 | false => "Single", 34 | } 35 | ); 36 | } 37 | 38 | pub fn new(cgb_features: bool) -> Self { 39 | CgbSpeedSwitch { 40 | armed: false, 41 | current_speed_is_double: false, 42 | cgb_features, 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/src/memory/mbcs/mbc1.rs: -------------------------------------------------------------------------------- 1 | use crate::cartridge::Cartridge; 2 | use crate::log; 3 | use crate::memory::battery_backed_ram::BatteryBackedRam; 4 | use crate::memory::mbcs::MBC; 5 | use crate::memory::rom::Rom; 6 | 7 | // 16KB (one bank size) in bytes 8 | pub const KB_16: usize = 16_384; 9 | 10 | pub struct MBC1 { 11 | pub rom: Rom, 12 | pub rom_bank: u8, 13 | 14 | pub ram: BatteryBackedRam, 15 | pub ram_enabled: bool, 16 | 17 | has_shown_ram_warning: bool, 18 | } 19 | 20 | impl MBC for MBC1 { 21 | fn read(&self, address: u16) -> u8 { 22 | match address { 23 | 0x0..=0x3FFF => self.read_bank(0, address), 24 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000), 25 | _ => panic!("Unsupported MBC1 read at {:#06x}", address), 26 | } 27 | } 28 | 29 | fn write(&mut self, address: u16, value: u8) { 30 | match address { 31 | 0x0000..=0x1FFF => { 32 | self.ram_enabled = (value & 0x0A) == 0x0A; 33 | }, 34 | 0x2000..=0x3FFF => { 35 | // TODO: Bank numbers are masked to the max bank number 36 | // TODO: Upper/RAM banking support 37 | let mut n = value & 0b11111; 38 | if n == 0 { 39 | n = 1 40 | } 41 | self.rom_bank = n 42 | }, 43 | // 0x4000 ..= 0x5FFF => { 44 | // panic!("Unsupported upper bank number or RAM banking in MBC1") 45 | // }, 46 | // 0x6000 ..= 0x7FFF => { 47 | // panic!("Unsupported MBC1 mode select write") 48 | // }, 49 | _ => {}, //panic!("Unsupported MBC1 write at {:#06x} (value: {:#04x})", address, value) 50 | } 51 | } 52 | 53 | fn ram_read(&self, address: u16) -> u8 { 54 | if !self.ram_enabled && !self.has_shown_ram_warning { 55 | log!("[WARN] MBC1 RAM read while disabled"); 56 | } 57 | 58 | // When an address outside of RAM space is read, the gameboy 59 | // doesn't seem to be intended to crash. 60 | // Not sure what to return here, but unusable RAM on the GB itself 61 | // returns 0xFF 62 | if address as usize >= self.ram.size { 63 | return 0xFF; 64 | } 65 | 66 | self.ram.read(address) 67 | } 68 | 69 | fn ram_write(&mut self, address: u16, value: u8) { 70 | if !self.ram_enabled && !self.has_shown_ram_warning { 71 | log!("[WARN] MBC1 RAM write while disabled"); 72 | // Otherwise the game is slowed down by constant debug printing 73 | self.has_shown_ram_warning = true; 74 | } 75 | 76 | // See note in ram_read 77 | if address as usize >= self.ram.size { 78 | return; 79 | } 80 | 81 | self.ram.write(address, value) 82 | } 83 | 84 | fn step(&mut self, ms_since_boot: usize) { 85 | self.ram.step(ms_since_boot) 86 | } 87 | } 88 | 89 | impl MBC1 { 90 | fn read_bank(&self, bank: u8, address: u16) -> u8 { 91 | let ub = bank as usize; 92 | let ua = address as usize; 93 | self.rom.bytes[KB_16 * ub + ua] 94 | } 95 | 96 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self { 97 | // TODO: Banked RAM 98 | if cart_info.ram_size > 8_192 { 99 | panic!("gbrs doesn't support banked (>=32K) MBC1 RAM"); 100 | } 101 | 102 | let has_battery = cart_info.cart_type == 0x03; 103 | MBC1 { 104 | rom, 105 | ram_enabled: false, 106 | rom_bank: 1, 107 | ram: BatteryBackedRam::new(cart_info, 0, has_battery), 108 | has_shown_ram_warning: false, 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /core/src/memory/mbcs/mbc2.rs: -------------------------------------------------------------------------------- 1 | use crate::cartridge::Cartridge; 2 | use crate::log; 3 | use crate::memory::battery_backed_ram::BatteryBackedRam; 4 | use crate::memory::mbcs::MBC; 5 | use crate::memory::rom::Rom; 6 | 7 | // 16KB (one bank size) in bytes 8 | pub const KB_16: usize = 16_384; 9 | 10 | pub struct MBC2 { 11 | pub rom: Rom, 12 | pub rom_bank: u8, 13 | 14 | pub ram: BatteryBackedRam, 15 | pub ram_enabled: bool, 16 | 17 | has_shown_ram_warning: bool, 18 | } 19 | 20 | impl MBC for MBC2 { 21 | fn read(&self, address: u16) -> u8 { 22 | match address { 23 | 0x0..=0x3FFF => self.read_bank(0, address), 24 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000), 25 | _ => panic!("Unsupported MBC2 read at {:#06x}", address), 26 | } 27 | } 28 | 29 | fn write(&mut self, address: u16, value: u8) { 30 | match address { 31 | 0x0000..=0x1FFF => { 32 | self.ram_enabled = (value & 0x0A) == 0x0A; 33 | }, 34 | 0x2000..=0x3FFF => { 35 | let mut n = value & 0b1111; 36 | if n == 0 { 37 | n = 1 38 | } 39 | self.rom_bank = n 40 | }, 41 | _ => {}, 42 | } 43 | } 44 | 45 | fn ram_read(&self, address: u16) -> u8 { 46 | if !self.ram_enabled && !self.has_shown_ram_warning { 47 | log!("[WARN] MBC2 RAM read while disabled"); 48 | } 49 | 50 | // When an address outside of RAM space is read, the gameboy 51 | // doesn't seem to be intended to crash. 52 | // Not sure what to return here, but unusable RAM on the GB itself 53 | // returns 0xFF 54 | if address as usize >= self.ram.size { 55 | return 0xFF; 56 | } 57 | 58 | self.ram.read(address) 59 | } 60 | 61 | fn ram_write(&mut self, address: u16, value: u8) { 62 | if !self.ram_enabled && !self.has_shown_ram_warning { 63 | log!("[WARN] MBC2 RAM write while disabled"); 64 | // Otherwise the game is slowed down by constant debug printing 65 | self.has_shown_ram_warning = true; 66 | } 67 | 68 | // See note in ram_read 69 | if address as usize >= self.ram.size { 70 | return; 71 | } 72 | 73 | // NOTE: Only the bottom 4 bits of the written byte are actually 74 | // saved on an MBC2, it's half-byte RAM. The top half is 75 | // undefined when read. 76 | self.ram.write(address, value & 0xF) 77 | } 78 | 79 | fn step(&mut self, ms_since_boot: usize) { 80 | self.ram.step(ms_since_boot) 81 | } 82 | } 83 | 84 | impl MBC2 { 85 | fn read_bank(&self, bank: u8, address: u16) -> u8 { 86 | let ub = bank as usize; 87 | let ua = address as usize; 88 | self.rom.bytes[KB_16 * ub + ua] 89 | } 90 | 91 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self { 92 | let has_battery = cart_info.cart_type == 0x06; 93 | MBC2 { 94 | rom, 95 | ram_enabled: false, 96 | rom_bank: 1, 97 | // The MBC2 always has 512 (half-)bytes of RAM built-in 98 | ram: BatteryBackedRam::new(cart_info, 512, has_battery), 99 | has_shown_ram_warning: false, 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/memory/mbcs/mbc3.rs: -------------------------------------------------------------------------------- 1 | use crate::cartridge::Cartridge; 2 | use crate::log; 3 | use crate::memory::battery_backed_ram::BatteryBackedRam; 4 | use crate::memory::mbcs::MBC; 5 | use crate::memory::rom::Rom; 6 | 7 | // 8KB (one RAM bank size) in bytes 8 | pub const KB_8: usize = 8_192; 9 | // 16KB (one ROM bank size) in bytes 10 | pub const KB_16: usize = KB_8 * 2; 11 | 12 | pub struct MBC3 { 13 | pub rom: Rom, 14 | pub rom_bank: u8, 15 | 16 | pub ram: BatteryBackedRam, 17 | pub ram_bank: u8, 18 | pub ram_enabled: bool, 19 | 20 | // Unique MBC3 feature, sometimes the RAM addresses can be set up to 21 | // read a Real Time Clock 22 | pub rtc_select: bool, 23 | 24 | has_shown_ram_warning: bool, 25 | } 26 | 27 | impl MBC for MBC3 { 28 | fn read(&self, address: u16) -> u8 { 29 | match address { 30 | 0x0..=0x3FFF => self.read_bank(0, address), 31 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000), 32 | _ => panic!("Unsupported MBC3 read at {:#06x}", address), 33 | } 34 | } 35 | 36 | fn write(&mut self, address: u16, value: u8) { 37 | match address { 38 | 0x0000..=0x1FFF => { 39 | self.ram_enabled = (value & 0x0A) == 0x0A; 40 | }, 41 | 0x2000..=0x3FFF => { 42 | let mut n = value & 0b01111111; 43 | let max_bank = (self.rom.bytes.len() / KB_16) as u8; 44 | let bitmask = max_bank - 1; 45 | n = n & bitmask; 46 | if n == 0 { 47 | n = 1 48 | } 49 | // log!("Selecting ROM bank {}", n); 50 | self.rom_bank = n 51 | }, 52 | 0x4000..=0x5FFF => { 53 | match value { 54 | 0x00..=0x03 => { 55 | // log!("Selecting RAM bank {}", value); 56 | self.ram_bank = value; 57 | self.rtc_select = false; 58 | }, 59 | // TODO: This maps Real Time Clock stuff 60 | 0x08..=0x0C => { 61 | self.rtc_select = true; 62 | }, 63 | // This is a noop 64 | _ => {}, 65 | } 66 | }, 67 | 0x6000..=0x7FFF => { 68 | // TODO: RTC latching 69 | }, 70 | _ => {}, 71 | } 72 | } 73 | 74 | fn ram_read(&self, address: u16) -> u8 { 75 | if !self.ram_enabled && !self.has_shown_ram_warning { 76 | // log!("[WARN] MBC3 RAM read while disabled"); 77 | } 78 | 79 | if self.rtc_select { 80 | // The game has opted to replace RAM with the value of the RTC. 81 | // TODO: Properly emulate the Real Time Clock 82 | // log!("Reading the RTC"); 83 | return 0; 84 | } 85 | 86 | self.read_ram_bank(self.ram_bank, address) 87 | } 88 | 89 | fn ram_write(&mut self, address: u16, value: u8) { 90 | if !self.ram_enabled && !self.has_shown_ram_warning { 91 | log!("[WARN] MBC3 RAM write while disabled"); 92 | // Otherwise the game is slowed down by constant debug printing 93 | self.has_shown_ram_warning = true; 94 | } 95 | 96 | self.write_ram_bank(self.ram_bank, address, value); 97 | } 98 | 99 | fn step(&mut self, ms_since_boot: usize) { 100 | self.ram.step(ms_since_boot) 101 | } 102 | } 103 | 104 | impl MBC3 { 105 | fn read_bank(&self, bank: u8, address: u16) -> u8 { 106 | let ub = bank as usize; 107 | let ua = address as usize; 108 | let final_addr = KB_16 * ub + ua; 109 | 110 | // if final_addr >= self.rom.bytes.len() { 111 | // return 0xFF; 112 | // } 113 | 114 | self.rom.bytes[final_addr] 115 | } 116 | 117 | fn read_ram_bank(&self, bank: u8, address: u16) -> u8 { 118 | let ub = bank as usize; 119 | let ua = address as usize; 120 | let final_addr = KB_8 * ub + ua; 121 | 122 | // if final_addr >= self.ram.size { 123 | // return 0xFF; 124 | // } 125 | 126 | self.ram.ram.bytes[final_addr] 127 | } 128 | 129 | fn write_ram_bank(&mut self, bank: u8, address: u16, value: u8) { 130 | let ub = bank as usize; 131 | let ua = address as usize; 132 | let final_addr = KB_8 * ub + ua; 133 | 134 | if final_addr >= self.ram.size { 135 | return; 136 | } 137 | 138 | self.ram.write(final_addr as u16, value); 139 | } 140 | 141 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self { 142 | let has_battery = cart_info.cart_type == 0x13; 143 | MBC3 { 144 | rom, 145 | rom_bank: 1, 146 | ram: BatteryBackedRam::new(cart_info, 0, has_battery), 147 | ram_bank: 0, 148 | ram_enabled: false, 149 | rtc_select: false, 150 | has_shown_ram_warning: false, 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /core/src/memory/mbcs/mbc5.rs: -------------------------------------------------------------------------------- 1 | use crate::cartridge::Cartridge; 2 | use crate::log; 3 | use crate::memory::battery_backed_ram::BatteryBackedRam; 4 | use crate::memory::mbcs::MBC; 5 | use crate::memory::rom::Rom; 6 | 7 | // 8KB (one RAM bank) in bytes 8 | pub const KB_8: usize = 8_192; 9 | // 16KB (one ROM bank) in bytes 10 | pub const KB_16: usize = 16_384; 11 | 12 | pub struct MBC5 { 13 | pub rom: Rom, 14 | pub rom_bank: u16, 15 | 16 | pub ram: BatteryBackedRam, 17 | pub ram_enabled: bool, 18 | pub ram_bank: u8, 19 | 20 | has_shown_ram_warning: bool, 21 | } 22 | 23 | impl MBC for MBC5 { 24 | fn read(&self, address: u16) -> u8 { 25 | match address { 26 | 0x0..=0x3FFF => self.read_bank(0, address), 27 | 0x4000..=0x7FFF => self.read_bank(self.rom_bank, address - 0x4000), 28 | _ => panic!("Unsupported MBC5 read at {:#06x}", address), 29 | } 30 | } 31 | 32 | fn write(&mut self, address: u16, value: u8) { 33 | match address { 34 | 0x0000..=0x1FFF => { 35 | self.ram_enabled = (value & 0x0A) == 0x0A; 36 | }, 37 | 0x2000..=0x2FFF => { 38 | // TODO: Are bank numbers masked to the max bank number? 39 | 40 | // No zero check. You can map bank 0 twice on MBC5. 41 | self.rom_bank = 42 | (self.rom_bank & 0b0000_0001_0000_0000) | (value as u16); 43 | }, 44 | 0x3000..=0x3FFF => { 45 | let bit = if value > 0 { 1 } else { 0 }; 46 | self.rom_bank = 47 | (self.rom_bank & 0b0000_0000_1111_1111) | (bit << 8); 48 | }, 49 | 0x4000..=0x5FFF => { 50 | // TODO: Rumble. If the MBC has rumble circuitry, this may be 51 | // wrong because we pass on bit 3. 52 | if value > 0x0F { 53 | return; 54 | } 55 | self.ram_bank = value; 56 | }, 57 | _ => {}, 58 | } 59 | } 60 | 61 | fn ram_read(&self, address: u16) -> u8 { 62 | if !self.ram_enabled && !self.has_shown_ram_warning { 63 | log!("[WARN] MBC5 RAM read while disabled"); 64 | } 65 | 66 | let banked_address = address as usize + self.ram_bank as usize * KB_8; 67 | 68 | if banked_address >= self.ram.size { 69 | return 0xFF; 70 | } 71 | 72 | self.ram.read_usize(banked_address) 73 | } 74 | 75 | fn ram_write(&mut self, address: u16, value: u8) { 76 | if !self.ram_enabled && !self.has_shown_ram_warning { 77 | log!("[WARN] MBC5 RAM write while disabled"); 78 | // Otherwise the game is slowed down by constant debug printing 79 | self.has_shown_ram_warning = true; 80 | } 81 | 82 | let banked_address = address as usize + self.ram_bank as usize * KB_8; 83 | 84 | if banked_address >= self.ram.size { 85 | return; 86 | } 87 | 88 | self.ram.write_usize(banked_address, value) 89 | } 90 | 91 | fn step(&mut self, ms_since_boot: usize) { 92 | self.ram.step(ms_since_boot) 93 | } 94 | } 95 | 96 | impl MBC5 { 97 | fn read_bank(&self, bank: u16, address: u16) -> u8 { 98 | let ub = bank as usize; 99 | let ua = address as usize; 100 | self.rom.bytes[KB_16 * ub + ua] 101 | } 102 | 103 | pub fn new(cart_info: Cartridge, rom: Rom) -> Self { 104 | // TODO: Banked RAM 105 | if cart_info.ram_size > 8_192 { 106 | panic!("gbrs doesn't support banked (>=32K) MBC5 RAM"); 107 | } 108 | 109 | let has_battery = cart_info.cart_type == 0x03; 110 | MBC5 { 111 | rom, 112 | rom_bank: 1, 113 | ram: BatteryBackedRam::new(cart_info, 0, has_battery), 114 | ram_enabled: false, 115 | ram_bank: 0, 116 | has_shown_ram_warning: false, 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /core/src/memory/mbcs/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::cartridge::Cartridge; 2 | use crate::log; 3 | use crate::memory::rom::Rom; 4 | 5 | #[cfg(not(feature = "std"))] 6 | use alloc::boxed::Box; 7 | 8 | pub trait MBC { 9 | fn read(&self, address: u16) -> u8; 10 | fn write(&mut self, address: u16, value: u8); 11 | 12 | fn ram_read(&self, address: u16) -> u8; 13 | fn ram_write(&mut self, address: u16, value: u8); 14 | 15 | // Mostly used to debounce battery-backed RAM saves 16 | fn step(&mut self, ms_since_boot: usize); 17 | } 18 | 19 | mod mbc1; 20 | mod mbc2; 21 | mod mbc3; 22 | mod mbc5; 23 | mod none; 24 | 25 | pub fn mbc_from_info(cart_info: Cartridge, rom: Rom) -> Box { 26 | log!("Loading game \"{}\"", cart_info.title); 27 | log!("Extra chips: {}", get_cart_type_string(&cart_info)); 28 | log!("ROM size: {}KB", cart_info.rom_size / 1024); 29 | log!("RAM size: {}KB", cart_info.ram_size / 1024); 30 | 31 | match cart_info.cart_type { 32 | 0x00 => Box::new(none::MBCNone::new(rom)), 33 | 0x01 ..= 0x03 => Box::new(mbc1::MBC1::new(cart_info, rom)), 34 | 0x05 ..= 0x06 => Box::new(mbc2::MBC2::new(cart_info, rom)), 35 | 0x0F ..= 0x13 => Box::new(mbc3::MBC3::new(cart_info, rom)), 36 | 0x19 ..= 0x1E => Box::new(mbc5::MBC5::new(cart_info, rom)), 37 | _ => panic!("gbrs doesn't support this cartridge's memory controller ({:#04x}).", cart_info.cart_type) 38 | } 39 | } 40 | 41 | fn get_cart_type_string(cart_info: &Cartridge) -> &str { 42 | match cart_info.cart_type { 43 | 0x00 => "None", 44 | 0x01 => "MBC1", 45 | 0x02 => "MBC1 + RAM", 46 | 0x03 => "MBC1 + RAM + BATTERY", 47 | // There are some gaps where Pan Docs doesn't define what they are 48 | 0x05 => "MBC2", 49 | 0x06 => "MBC2 + BATTERY", 50 | 51 | 0x08 => "ROM + RAM (Unofficial)", // No gameboy game uses these 52 | 0x09 => "ROM + RAM + BATTERY (Unofficial)", 53 | 54 | 0x0B => "MMM01", 55 | 0x0C => "MMM01 + RAM", 56 | 0x0D => "MMM01 + RAM + BATTERY", 57 | 58 | 0x0F => "MBC3 + TIMER + BATTERY", 59 | 0x10 => "MBC3 + TIMER + RAM + BATTERY", 60 | 0x11 => "MBC3", 61 | 0x12 => "MBC3 + RAM", 62 | 0x13 => "MBC3 + RAM + BATTERY", 63 | 64 | // There is no MBC4. There is superstition about the number 4 in Japan. 65 | 66 | 0x19 => "MBC5", 67 | 0x1A => "MBC5 + RAM", 68 | 0x1B => "MBC5 + RAM + BATTERY", 69 | 0x1C => "MBC5 + RUMBLE", 70 | 0x1D => "MBC5 + RUMBLE + RAM", 71 | 0x1E => "MBC5 + RUMBLE + RAM + BATTERY", 72 | 73 | _ => panic!("gbrs doesn't know the name of this cartridge's memory controller ({:#04x}).", cart_info.cart_type) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /core/src/memory/mbcs/none.rs: -------------------------------------------------------------------------------- 1 | use crate::memory::mbcs::MBC; 2 | use crate::memory::rom::Rom; 3 | 4 | pub struct MBCNone { 5 | pub rom: Rom, 6 | } 7 | 8 | impl MBC for MBCNone { 9 | fn read(&self, address: u16) -> u8 { 10 | self.rom.read(address) 11 | } 12 | 13 | fn write(&mut self, _: u16, _: u8) { 14 | // No MBC ignores writes 15 | } 16 | 17 | fn ram_read(&self, _: u16) -> u8 { 18 | // Unused Gameboy RAM reads as 0xFF 19 | 0xFF 20 | } 21 | 22 | fn ram_write(&mut self, _: u16, _: u8) { 23 | // We don't have RAM 24 | } 25 | 26 | fn step(&mut self, _ms_since_boot: usize) { 27 | // We don't need to do anything here 28 | } 29 | } 30 | 31 | impl MBCNone { 32 | pub fn new(rom: Rom) -> Self { 33 | MBCNone { rom } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/memory/memory.rs: -------------------------------------------------------------------------------- 1 | use crate::cartridge::Cartridge; 2 | use crate::colour::palette_ram::PaletteRam; 3 | use crate::constants::*; 4 | use crate::cpu::EmulationTarget; 5 | use crate::gpu::Gpu; 6 | use crate::interrupts::*; 7 | use crate::joypad::Joypad; 8 | use crate::log; 9 | use crate::memory::cgb_speed_switch::CgbSpeedSwitch; 10 | use crate::memory::mbcs::*; 11 | use crate::memory::ram::Ram; 12 | use crate::memory::rom::Rom; 13 | use crate::memory::vram::VRam; 14 | use crate::serial_cable::SerialCable; 15 | use crate::sound::apu::APU; 16 | use crate::{combine_u8, split_u16}; 17 | 18 | #[cfg(not(feature = "std"))] 19 | use alloc::boxed::Box; 20 | 21 | // TODO: Rename this to something more appropriate 22 | // (I've seen an emu call a similar struct 'Interconnect') 23 | pub struct Memory { 24 | cgb_features: bool, 25 | 26 | mbc: Box, 27 | 28 | // TODO: Move VRAM to GPU? 29 | pub vram: VRam, 30 | // Includes all banks contiguously 31 | wram: Ram, 32 | // On DMG, this is always 1. On CGB, it's 1-7 inclusive 33 | upper_wram_bank: usize, 34 | hram: Ram, 35 | // Used in CGB mode only 36 | pub palette_ram: PaletteRam, 37 | 38 | serial_cable: SerialCable, 39 | 40 | timer_divider_increase: u16, 41 | timer_divider: u8, 42 | 43 | timer_counter_increase: u32, 44 | timer_counter: u8, 45 | 46 | timer_modulo: u8, 47 | 48 | timer_control: u8, 49 | 50 | pub joypad: Joypad, 51 | 52 | pub apu: APU, 53 | pub speed_switch: CgbSpeedSwitch, 54 | } 55 | 56 | impl Memory { 57 | // Memory has a step command for timers & MBCs 58 | pub fn step( 59 | &mut self, 60 | cycles: usize, 61 | ints: &mut Interrupts, 62 | ms_since_boot: usize, 63 | ) { 64 | // These two timers are safe to implement like this vs per-cycle 65 | // because the CPU will never do more than about 16 cycles per step, 66 | // let alone >256 67 | self.timer_divider_increase += cycles as u16; 68 | if self.timer_divider_increase >= 256 { 69 | self.timer_divider_increase -= 256; 70 | self.timer_divider = self.timer_divider.wrapping_add(1); 71 | } 72 | 73 | let enabled = (self.timer_control >> 2) == 1; 74 | if enabled { 75 | self.timer_counter_increase += cycles as u32; 76 | 77 | let step = match self.timer_control & 0b11 { 78 | 0b00 => 1024, 79 | 0b01 => 16, 80 | 0b10 => 64, 81 | 0b11 => 256, 82 | _ => unreachable!(), 83 | }; 84 | 85 | while self.timer_counter_increase >= step { 86 | self.timer_counter = self.timer_counter.wrapping_add(1); 87 | if self.timer_counter == 0 { 88 | self.timer_counter = self.timer_modulo; 89 | ints.raise_interrupt(InterruptReason::Timer); 90 | } 91 | self.timer_counter_increase -= step; 92 | } 93 | } 94 | 95 | self.serial_cable.step(ints, cycles); 96 | 97 | self.mbc.step(ms_since_boot); 98 | } 99 | 100 | #[inline(always)] 101 | pub fn read(&self, ints: &Interrupts, gpu: &Gpu, address: u16) -> u8 { 102 | match address { 103 | // Cartridge memory starts at the 0 address 104 | 0..=MBC_ROM_END => self.mbc.read(address), 105 | 106 | VRAM_START..=VRAM_END => self.vram.raw_read(address), 107 | 108 | MBC_RAM_START..=MBC_RAM_END => { 109 | self.mbc.ram_read(address - MBC_RAM_START) 110 | }, 111 | 112 | WRAM_LOWER_BANK_START..=WRAM_LOWER_BANK_END => { 113 | self.wram.read(address - WRAM_LOWER_BANK_START) 114 | }, 115 | WRAM_UPPER_BANK_START..=WRAM_UPPER_BANK_END => { 116 | self.wram.bytes[self.upper_wram_bank * WRAM_BANK_SIZE 117 | + (address - WRAM_UPPER_BANK_START) as usize] 118 | }, 119 | // TODO: How does upper echo RAM work with CGB bank switching? 120 | ECHO_RAM_START..=ECHO_RAM_END => self.read( 121 | ints, 122 | gpu, 123 | address - (ECHO_RAM_START - WRAM_LOWER_BANK_START), 124 | ), 125 | 126 | OAM_START..=OAM_END => gpu.raw_read(address), 127 | 128 | UNUSABLE_MEMORY_START..=UNUSABLE_MEMORY_END => 0xFF, 129 | 130 | LINK_CABLE_SB | LINK_CABLE_SC => self.serial_cable.read(address), 131 | 132 | APU_START..=APU_END => self.apu.read(address), 133 | 134 | LCD_DATA_START..=LCD_DATA_END => gpu.raw_read(address), 135 | CGB_DMA_START..=CGB_DMA_END => gpu.raw_read(address), 136 | CGB_PALETTE_DATA_START..=CGB_PALETTE_DATA_END => { 137 | self.palette_ram.raw_read(address) 138 | }, 139 | HRAM_START..=HRAM_END => self.hram.read(address - HRAM_START), 140 | 141 | 0xFF00 => self.joypad.read(), 142 | 143 | // Timers 144 | 0xFF04 => self.timer_divider, 145 | 0xFF05 => self.timer_counter, 146 | 0xFF06 => self.timer_modulo, 147 | 0xFF07 => self.timer_control, 148 | 149 | 0xFF4D => self.speed_switch.read_switch_byte(), 150 | 151 | 0xFF4F => self.vram.bank as u8, 152 | 153 | 0xFF70 => self.upper_wram_bank as u8, 154 | 155 | INTERRUPT_ENABLE_ADDRESS => ints.enable_read(), 156 | INTERRUPT_FLAG_ADDRESS => ints.flag_read(), 157 | 158 | _ => { 159 | log!("Unsupported memory read at {:#06x}", address); 160 | 0xFF 161 | }, 162 | } 163 | } 164 | 165 | #[inline(always)] 166 | // Function complexity warning here is due to the massive switch statement. 167 | // Such a thing is expected in an emulator. 168 | // skipcq: RS-R1000 169 | pub fn write( 170 | &mut self, 171 | ints: &mut Interrupts, 172 | gpu: &mut Gpu, 173 | address: u16, 174 | value: u8, 175 | ) { 176 | match address { 177 | 0..=MBC_ROM_END => self.mbc.write(address, value), 178 | 179 | // TODO: Disable writing to VRAM if GPU is reading it 180 | VRAM_START..=VRAM_END => self.vram.raw_write(address, value), 181 | 182 | MBC_RAM_START..=MBC_RAM_END => { 183 | self.mbc.ram_write(address - MBC_RAM_START, value) 184 | }, 185 | 186 | WRAM_LOWER_BANK_START..=WRAM_LOWER_BANK_END => { 187 | self.wram.write(address - WRAM_LOWER_BANK_START, value) 188 | }, 189 | // CGB WRAM is so big that upper bank addresses might not fit into a u16, 190 | // so we'll do this directly with a usize 191 | WRAM_UPPER_BANK_START..=WRAM_UPPER_BANK_END => { 192 | self.wram.bytes[self.upper_wram_bank * WRAM_BANK_SIZE 193 | + (address - WRAM_UPPER_BANK_START) as usize] = value 194 | }, 195 | ECHO_RAM_START..=ECHO_RAM_END => self.write( 196 | ints, 197 | gpu, 198 | address - (ECHO_RAM_START - WRAM_LOWER_BANK_START), 199 | value, 200 | ), 201 | 202 | OAM_START..=OAM_END => gpu.raw_write(address, value, ints), 203 | 204 | // TETRIS writes here.. due to a bug 205 | UNUSABLE_MEMORY_START..=UNUSABLE_MEMORY_END => {}, 206 | 207 | LINK_CABLE_SB | LINK_CABLE_SC => { 208 | self.serial_cable.write(address, value) 209 | }, 210 | 211 | APU_START..=APU_END => self.apu.write(address, value), 212 | 213 | LCD_DATA_START..=LCD_DATA_END => { 214 | gpu.raw_write(address, value, ints) 215 | }, 216 | CGB_DMA_START..=CGB_DMA_END => gpu.raw_write(address, value, ints), 217 | CGB_PALETTE_DATA_START..=CGB_PALETTE_DATA_END => { 218 | self.palette_ram.raw_write(address, value) 219 | }, 220 | HRAM_START..=HRAM_END => { 221 | self.hram.write(address - HRAM_START, value) 222 | }, 223 | 224 | 0xFF00 => self.joypad.write(value), 225 | 226 | // Timers 227 | 0xFF04 => self.timer_divider = 0, 228 | // NOTE: This goes to 0 when written to, not to value 229 | 0xFF05 => self.timer_counter = 0, 230 | 0xFF06 => self.timer_modulo = value, 231 | 0xFF07 => self.timer_control = value, 232 | 233 | 0xFF4D => self.speed_switch.write_switch_byte(value), 234 | 235 | // VRAM bank select 236 | 0xFF4F => self.vram.bank_write(value), 237 | 238 | // Upper WRAM bank select 239 | 0xFF70 => { 240 | if !self.cgb_features { 241 | return; 242 | } 243 | let mut desired_bank = value & 0x07; 244 | if desired_bank == 0 { 245 | desired_bank = 1; 246 | } 247 | self.upper_wram_bank = desired_bank as usize; 248 | }, 249 | 250 | // TETRIS also writes here, Sameboy doesn't seem to care 251 | 0xFF7F => {}, 252 | 253 | INTERRUPT_ENABLE_ADDRESS => ints.enable_write(value), 254 | INTERRUPT_FLAG_ADDRESS => ints.flag_write(value), 255 | 256 | _ => log!( 257 | "Unsupported memory write at {:#06x} (value: {:#04x})", 258 | address, 259 | value 260 | ), 261 | } 262 | } 263 | 264 | #[inline(always)] 265 | pub fn read_16(&self, ints: &Interrupts, gpu: &Gpu, address: u16) -> u16 { 266 | combine_u8!( 267 | self.read(ints, gpu, address + 1), 268 | self.read(ints, gpu, address) 269 | ) 270 | } 271 | #[inline(always)] 272 | pub fn write_16( 273 | &mut self, 274 | ints: &mut Interrupts, 275 | gpu: &mut Gpu, 276 | address: u16, 277 | value: u16, 278 | ) { 279 | let (b1, b2) = split_u16!(value); 280 | self.write(ints, gpu, address, b1); 281 | self.write(ints, gpu, address + 1, b2); 282 | } 283 | 284 | pub fn from_info( 285 | cart_info: Cartridge, 286 | rom: Rom, 287 | target: &EmulationTarget, 288 | ) -> Memory { 289 | let cgb_features = target.has_cgb_features(); 290 | Memory { 291 | cgb_features, 292 | mbc: mbc_from_info(cart_info, rom), 293 | vram: VRam::new(cgb_features), 294 | wram: Ram::new(WRAM_BANK_SIZE * 8), 295 | upper_wram_bank: 1, 296 | hram: Ram::new(HRAM_SIZE), 297 | palette_ram: PaletteRam::new(&target), 298 | serial_cable: SerialCable::new(), 299 | timer_divider_increase: 0, 300 | timer_divider: 0, 301 | timer_counter_increase: 0, 302 | timer_counter: 0, 303 | timer_control: 0b00000010, 304 | timer_modulo: 0, 305 | joypad: Joypad::new(), 306 | apu: APU::new(), 307 | speed_switch: CgbSpeedSwitch::new(cgb_features), 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /core/src/memory/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod battery_backed_ram; 2 | pub mod cgb_speed_switch; 3 | pub mod mbcs; 4 | pub mod memory; 5 | pub mod ram; 6 | pub mod rom; 7 | pub mod vram; 8 | -------------------------------------------------------------------------------- /core/src/memory/ram.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "std"))] 2 | use alloc::{vec, vec::Vec}; 3 | 4 | pub struct Ram { 5 | pub bytes: Vec, 6 | pub size: usize, 7 | } 8 | 9 | impl Ram { 10 | #[inline(always)] 11 | pub fn read(&self, address: u16) -> u8 { 12 | self.bytes[address as usize] 13 | } 14 | 15 | #[inline(always)] 16 | pub fn write(&mut self, address: u16, value: u8) { 17 | self.bytes[address as usize] = value; 18 | } 19 | 20 | pub fn new(size: usize) -> Ram { 21 | Ram::with_filled_value(size, 0) 22 | } 23 | 24 | pub fn with_filled_value(size: usize, default_value: u8) -> Ram { 25 | Ram { 26 | bytes: vec![default_value; size], 27 | size, 28 | } 29 | } 30 | 31 | pub fn from_bytes(bytes: Vec, expected_size: usize) -> Ram { 32 | if bytes.len() != expected_size { 33 | panic!("Save file was not the expected length") 34 | } 35 | 36 | Ram { 37 | bytes, 38 | size: expected_size, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/memory/rom.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | use std::{fs::File, io::Read}; 3 | 4 | #[cfg(not(feature = "std"))] 5 | use alloc::{string::String, vec::Vec}; 6 | 7 | #[derive(Clone)] 8 | pub struct Rom { 9 | pub bytes: Vec, 10 | pub path: String, 11 | } 12 | 13 | impl Rom { 14 | #[inline(always)] 15 | pub fn read(&self, address: u16) -> u8 { 16 | self.bytes[address as usize] 17 | } 18 | 19 | #[cfg(feature = "std")] 20 | pub fn from_file(path: &str) -> Rom { 21 | let mut buffer = vec![]; 22 | let mut file = File::open(path).expect("Invalid ROM path"); 23 | file.read_to_end(&mut buffer) 24 | .expect("Unable to read ROM file"); 25 | 26 | Rom { 27 | bytes: buffer, 28 | path: path.to_string(), 29 | } 30 | } 31 | 32 | pub fn from_bytes(bytes: Vec) -> Rom { 33 | Rom { 34 | bytes, 35 | path: String::default(), 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/memory/vram.rs: -------------------------------------------------------------------------------- 1 | use super::ram::Ram; 2 | use crate::colour::bg_map_attributes::BgMapAttributeTable; 3 | use crate::constants::*; 4 | 5 | pub struct VRam { 6 | cgb_features: bool, 7 | memory: Ram, 8 | pub bank: u16, 9 | pub bg_map_attributes: BgMapAttributeTable, 10 | } 11 | 12 | impl VRam { 13 | pub fn read_arbitrary_bank(&self, bank: u16, address: u16) -> u8 { 14 | let relative_address = address - VRAM_START; 15 | self.memory 16 | .read(bank * VRAM_BANK_SIZE as u16 + relative_address) 17 | } 18 | 19 | pub fn raw_read(&self, address: u16) -> u8 { 20 | if self.bank == 1 && address > VRAM_BG_MAP_START { 21 | return self.bg_map_attributes.read(address - VRAM_BG_MAP_START); 22 | } 23 | 24 | let relative_address = address - VRAM_START; 25 | self.memory 26 | .read(self.bank * VRAM_BANK_SIZE as u16 + relative_address) 27 | } 28 | 29 | pub fn raw_write(&mut self, address: u16, value: u8) { 30 | if self.bank == 1 && address > VRAM_BG_MAP_START { 31 | // Attribute table 32 | self.bg_map_attributes 33 | .write(address - VRAM_BG_MAP_START, value); 34 | return; 35 | } 36 | 37 | let relative_address = address - VRAM_START; 38 | self.memory 39 | .write(self.bank * VRAM_BANK_SIZE as u16 + relative_address, value) 40 | } 41 | 42 | pub fn bank_write(&mut self, value: u8) { 43 | if !self.cgb_features { 44 | return; 45 | } 46 | self.bank = value as u16 & 0x01; 47 | } 48 | 49 | pub fn new(cgb_features: bool) -> VRam { 50 | VRam { 51 | cgb_features, 52 | memory: Ram::new(VRAM_BANK_SIZE * 2), 53 | bank: 0, 54 | bg_map_attributes: BgMapAttributeTable::new(), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/registers.rs: -------------------------------------------------------------------------------- 1 | use crate::cpu::EmulationTarget; 2 | use crate::gpu::Gpu; 3 | use crate::interrupts::*; 4 | use crate::memory::memory::Memory; 5 | use crate::{combine_u8, set_bit, split_u16}; 6 | 7 | #[cfg(not(feature = "std"))] 8 | use alloc::{format, string::String}; 9 | 10 | pub struct Registers { 11 | pub a: u8, 12 | pub b: u8, 13 | pub c: u8, 14 | pub d: u8, 15 | pub e: u8, 16 | pub f: u8, 17 | pub h: u8, 18 | pub l: u8, 19 | 20 | pub sp: u16, 21 | pub pc: u16, 22 | } 23 | impl Registers { 24 | fn set_flag(&mut self, flag_index: u8, bit: u8) { 25 | set_bit!(self.f, 4 + flag_index, bit); 26 | } 27 | pub fn set_carry_flag(&mut self, bit: u8) { 28 | self.set_flag(0, bit) 29 | } 30 | pub fn set_half_carry_flag(&mut self, bit: u8) { 31 | self.set_flag(1, bit) 32 | } 33 | pub fn set_operation_flag(&mut self, bit: u8) { 34 | self.set_flag(2, bit) 35 | } 36 | pub fn set_zero_flag(&mut self, bit: u8) { 37 | self.set_flag(3, bit) 38 | } 39 | 40 | pub fn set_flags( 41 | &mut self, 42 | zero: u8, 43 | operation: u8, 44 | half_carry: u8, 45 | carry: u8, 46 | ) { 47 | self.set_carry_flag(carry); 48 | self.set_half_carry_flag(half_carry); 49 | self.set_operation_flag(operation); 50 | self.set_zero_flag(zero); 51 | } 52 | 53 | fn get_flag(&self, flag_index: u8) -> u8 { 54 | (self.f >> (4 + flag_index)) & 0x1 55 | } 56 | pub fn get_carry_flag(&self) -> u8 { 57 | self.get_flag(0) 58 | } 59 | pub fn get_half_carry_flag(&self) -> u8 { 60 | self.get_flag(1) 61 | } 62 | pub fn get_operation_flag(&self) -> u8 { 63 | self.get_flag(2) 64 | } 65 | pub fn get_zero_flag(&self) -> u8 { 66 | self.get_flag(3) 67 | } 68 | 69 | pub fn get_af(&self) -> u16 { 70 | combine_u8!(self.a, self.f) 71 | } 72 | pub fn set_af(&mut self, value: u16) { 73 | let (b1, b2) = split_u16!(value); 74 | self.a = b2; 75 | self.f = b1 & 0xF0; 76 | } 77 | 78 | pub fn get_bc(&self) -> u16 { 79 | combine_u8!(self.b, self.c) 80 | } 81 | pub fn set_bc(&mut self, value: u16) { 82 | let (b1, b2) = split_u16!(value); 83 | self.b = b2; 84 | self.c = b1; 85 | } 86 | 87 | pub fn get_de(&self) -> u16 { 88 | combine_u8!(self.d, self.e) 89 | } 90 | pub fn set_de(&mut self, value: u16) { 91 | let (b1, b2) = split_u16!(value); 92 | self.d = b2; 93 | self.e = b1; 94 | } 95 | 96 | pub fn get_hl(&self) -> u16 { 97 | combine_u8!(self.h, self.l) 98 | } 99 | pub fn set_hl(&mut self, value: u16) { 100 | let (b1, b2) = split_u16!(value); 101 | self.h = b2; 102 | self.l = b1; 103 | } 104 | 105 | // These are left to right from the "GoldenCrystal Gameboy Z80 CPU Opcodes" PDF 106 | // The "sp" flag indicates whether 0b11 refers to the SP or AF 107 | #[inline(always)] 108 | fn set_combined_register_base( 109 | &mut self, 110 | register: u8, 111 | value: u16, 112 | sp: bool, 113 | ) { 114 | match register { 115 | 0b00 => self.set_bc(value), 116 | 0b01 => self.set_de(value), 117 | 0b10 => self.set_hl(value), 118 | 0b11 => { 119 | if sp { 120 | self.sp = value 121 | } else { 122 | self.set_af(value) 123 | } 124 | }, 125 | _ => panic!("Invalid combined register set"), 126 | } 127 | } 128 | #[inline(always)] 129 | fn get_combined_register_base(&self, register: u8, sp: bool) -> u16 { 130 | match register { 131 | 0b00 => self.get_bc(), 132 | 0b01 => self.get_de(), 133 | 0b10 => self.get_hl(), 134 | 0b11 => { 135 | if sp { 136 | self.sp 137 | } else { 138 | self.get_af() 139 | } 140 | }, 141 | _ => panic!("Invalid combined register get"), 142 | } 143 | } 144 | 145 | #[inline(always)] 146 | pub fn get_combined_register(&self, register: u8) -> u16 { 147 | self.get_combined_register_base(register, true) 148 | } 149 | #[inline(always)] 150 | pub fn set_combined_register(&mut self, register: u8, value: u16) { 151 | self.set_combined_register_base(register, value, true) 152 | } 153 | #[inline(always)] 154 | pub fn get_combined_register_alt(&self, register: u8) -> u16 { 155 | self.get_combined_register_base(register, false) 156 | } 157 | #[inline(always)] 158 | pub fn set_combined_register_alt(&mut self, register: u8, value: u16) { 159 | self.set_combined_register_base(register, value, false) 160 | } 161 | 162 | #[inline(always)] 163 | pub fn set_singular_register( 164 | &mut self, 165 | register: u8, 166 | value: u8, 167 | mem: &mut Memory, 168 | ints: &mut Interrupts, 169 | gpu: &mut Gpu, 170 | ) { 171 | match register { 172 | 0b000 => self.b = value, 173 | 0b001 => self.c = value, 174 | 0b010 => self.d = value, 175 | 0b011 => self.e = value, 176 | 0b100 => self.h = value, 177 | 0b101 => self.l = value, 178 | 0b110 => mem.write(ints, gpu, self.get_hl(), value), 179 | 0b111 => self.a = value, 180 | _ => panic!("Invalid singular register set"), 181 | } 182 | } 183 | 184 | #[inline(always)] 185 | pub fn get_singular_register( 186 | &self, 187 | register: u8, 188 | mem: &Memory, 189 | ints: &Interrupts, 190 | gpu: &Gpu, 191 | ) -> u8 { 192 | match register { 193 | 0b000 => self.b, 194 | 0b001 => self.c, 195 | 0b010 => self.d, 196 | 0b011 => self.e, 197 | 0b100 => self.h, 198 | 0b101 => self.l, 199 | 0b110 => mem.read(ints, gpu, self.get_hl()), 200 | 0b111 => self.a, 201 | _ => panic!("Invalid singular register get"), 202 | } 203 | } 204 | 205 | pub fn debug_dump(&self) -> String { 206 | format!( 207 | "AF: {:#06x} | BC: {:#06x} | DE: {:#06x} | HL: {:#06x}", 208 | self.get_af(), 209 | self.get_bc(), 210 | self.get_de(), 211 | self.get_hl() 212 | ) 213 | } 214 | 215 | pub fn new(emulation_target: &EmulationTarget) -> Registers { 216 | // NOTE: These values are what's in the registers after the boot rom, 217 | // since we don't run that. 218 | // This is how games detect that they can use GameBoy Color features. 219 | let bootup_a_value = match emulation_target { 220 | EmulationTarget::Dmg | EmulationTarget::CgbDmgMode => 0x01, 221 | EmulationTarget::CgbCgbMode | EmulationTarget::GbaCgbMode => 0x11, 222 | }; 223 | // This is exclusively used to detect running on the GameBoy Advance. 224 | let bootup_b_value = match emulation_target { 225 | EmulationTarget::GbaCgbMode => 0x01, 226 | _ => 0x00, 227 | }; 228 | Registers { 229 | a: bootup_a_value, 230 | b: bootup_b_value, 231 | c: 0x13, 232 | d: 0x00, 233 | e: 0xD8, 234 | f: 0xB0, 235 | h: 0x01, 236 | l: 0x4D, 237 | sp: 0xFFFE, 238 | pc: 0x100, 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /core/src/serial_cable.rs: -------------------------------------------------------------------------------- 1 | // Gameboy Link Cable 2 | // Mostly a stub, although some parts have to be emulated *somewhat* accurately 3 | // to emulate fussy games like Alleyway 4 | use crate::constants::*; 5 | use crate::interrupts::{InterruptReason, Interrupts}; 6 | 7 | // Unusual serial code inspired by 8 | // https://github.com/rvaccarim/FrozenBoy/blob/master/FrozenBoyCore/Serial/SerialLink.cs 9 | 10 | pub struct SerialCable { 11 | transfer_data_byte: u8, 12 | transfer_control_byte: u8, 13 | 14 | counter: usize, 15 | transfer_in_progress: bool, 16 | } 17 | 18 | impl SerialCable { 19 | pub fn read(&self, address: u16) -> u8 { 20 | match address { 21 | // When there's no gameboy on the other end, this apparently 22 | // just always reads 0xFF 23 | LINK_CABLE_SB => self.transfer_data_byte, 24 | LINK_CABLE_SC => self.transfer_control_byte | 0b1111_1110, 25 | _ => unreachable!(), 26 | } 27 | } 28 | 29 | pub fn write(&mut self, address: u16, value: u8) { 30 | match address { 31 | LINK_CABLE_SB => self.transfer_data_byte = value, 32 | LINK_CABLE_SC => { 33 | self.transfer_control_byte = value; 34 | 35 | if value == 0x81 { 36 | self.transfer_in_progress = true; 37 | self.counter = 0; 38 | } 39 | }, 40 | _ => unreachable!(), 41 | } 42 | } 43 | 44 | pub fn step(&mut self, ints: &mut Interrupts, cycles: usize) { 45 | if !self.transfer_in_progress { 46 | return; 47 | } 48 | 49 | self.counter += cycles; 50 | 51 | if self.counter >= 512 { 52 | self.transfer_in_progress = false; 53 | self.transfer_data_byte = 0xFF; 54 | ints.raise_interrupt(InterruptReason::Serial); 55 | } 56 | } 57 | 58 | pub fn new() -> SerialCable { 59 | SerialCable { 60 | transfer_data_byte: 0, 61 | transfer_control_byte: 0, 62 | 63 | counter: 0, 64 | transfer_in_progress: false, 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/src/sound/apu.rs: -------------------------------------------------------------------------------- 1 | use super::channel1::APUChannel1; 2 | use super::channel2::APUChannel2; 3 | use super::channel3::APUChannel3; 4 | use super::channel4::APUChannel4; 5 | use super::registers::*; 6 | use crate::constants::*; 7 | 8 | pub trait APUChannel { 9 | fn step(&mut self); 10 | fn sample(&self) -> f32; 11 | fn read(&self, address: u16) -> u8; 12 | fn write(&mut self, address: u16, value: u8); 13 | } 14 | 15 | // Audio processing unit 16 | // NOTE: Max APU frequency seems to be 131072 Hz 17 | pub struct APU { 18 | pub stereo_left_volume: f32, 19 | pub stereo_right_volume: f32, 20 | 21 | pub stereo_panning: StereoPanning, 22 | 23 | pub sound_on_register: u8, 24 | 25 | pub channel1: APUChannel1, 26 | pub channel2: APUChannel2, 27 | pub channel3: APUChannel3, 28 | pub channel4: APUChannel4, 29 | 30 | pub sample_counter: usize, 31 | // This could be a Vec that we check len() against, but we can save the 32 | // allocation because we know the size it's always going to be. 33 | pub buffer: [i16; SOUND_BUFFER_SIZE], 34 | pub buffer_idx: usize, 35 | pub buffer_full: bool, 36 | } 37 | 38 | impl APU { 39 | pub fn step(&mut self) { 40 | self.channel1.step(); 41 | self.channel2.step(); 42 | self.channel3.step(); 43 | self.channel4.step(); 44 | 45 | self.sample_counter += 1; 46 | 47 | if self.sample_counter == APU_SAMPLE_CLOCKS { 48 | self.sample_counter = 0; 49 | self.sample(); 50 | } 51 | } 52 | 53 | pub fn sample(&mut self) { 54 | let mut left_sample = 0.; 55 | let mut right_sample = 0.; 56 | 57 | // TODO: Maybe we could generate these with a macro? 58 | let chan1 = self.channel1.sample(); 59 | if self.stereo_panning.channel1_left { 60 | left_sample += chan1; 61 | } 62 | if self.stereo_panning.channel1_right { 63 | right_sample += chan1; 64 | } 65 | 66 | let chan2 = self.channel2.sample(); 67 | if self.stereo_panning.channel2_left { 68 | left_sample += chan2; 69 | } 70 | if self.stereo_panning.channel2_right { 71 | right_sample += chan2; 72 | } 73 | 74 | let chan3 = self.channel3.sample(); 75 | if self.stereo_panning.channel3_left { 76 | left_sample += chan3; 77 | } 78 | if self.stereo_panning.channel3_right { 79 | right_sample += chan3; 80 | } 81 | 82 | let chan4 = self.channel4.sample(); 83 | if self.stereo_panning.channel4_left { 84 | left_sample += chan4; 85 | } 86 | if self.stereo_panning.channel4_right { 87 | right_sample += chan4; 88 | } 89 | 90 | // Average the 4 channels 91 | left_sample /= 4.; 92 | right_sample /= 4.; 93 | 94 | // Adjust for soft-panning 95 | left_sample *= self.stereo_left_volume; 96 | right_sample *= self.stereo_right_volume; 97 | 98 | let left_sample_int = (left_sample * 30_000.) as i16; 99 | let right_sample_int = (right_sample * 30_000.) as i16; 100 | 101 | self.buffer[self.buffer_idx] = left_sample_int; 102 | self.buffer_idx += 1; 103 | self.buffer[self.buffer_idx] = right_sample_int; 104 | self.buffer_idx += 1; 105 | 106 | if self.buffer_idx == SOUND_BUFFER_SIZE { 107 | self.buffer_idx = 0; 108 | self.buffer_full = true; 109 | } 110 | } 111 | 112 | #[allow(unused_variables)] 113 | #[allow(unreachable_code)] 114 | pub fn read(&self, address: u16) -> u8 { 115 | #[cfg(not(feature = "sound"))] 116 | return 0; 117 | 118 | match address { 119 | 0xFF24 => self.serialise_nr50(), 120 | 0xFF25 => u8::from(self.stereo_panning.clone()), 121 | 0xFF26 => self.sound_on_register, 122 | 123 | 0xFF10..=0xFF14 => self.channel1.read(address), 124 | 0xFF16..=0xFF19 => self.channel2.read(address), 125 | 0xFF1A..=0xFF1E => self.channel3.read(address), 126 | 0xFF20..=0xFF23 => self.channel4.read(address), 127 | 128 | WAVE_RAM_START..=WAVE_RAM_END => self.channel3.read(address), 129 | _ => 0, //panic!("Unknown read {:#06x} in APU", address) 130 | } 131 | } 132 | 133 | #[allow(unused_variables)] 134 | #[allow(unreachable_code)] 135 | pub fn write(&mut self, address: u16, value: u8) { 136 | #[cfg(not(feature = "sound"))] 137 | return; 138 | 139 | match address { 140 | 0xFF24 => self.deserialise_nr50(value), 141 | 0xFF25 => self.stereo_panning = StereoPanning::from(value), 142 | 0xFF26 => self.sound_on_register = value, 143 | 144 | 0xFF10..=0xFF14 => self.channel1.write(address, value), 145 | 0xFF16..=0xFF19 => self.channel2.write(address, value), 146 | 0xFF1A..=0xFF1E => self.channel3.write(address, value), 147 | 0xFF20..=0xFF23 => self.channel4.write(address, value), 148 | 149 | WAVE_RAM_START..=WAVE_RAM_END => { 150 | self.channel3.write(address, value) 151 | }, 152 | _ => {}, //log!("Unknown write {:#06x} (value: {:#04}) in APU", address, value) 153 | } 154 | } 155 | 156 | // NOTE: These functions don't take into account the 157 | // Vin output flags. That feature is unused in all 158 | // commercial Gameboy games, so we ignore it. 159 | fn deserialise_nr50(&mut self, nr50: u8) { 160 | let right_vol = nr50 & 0b111; 161 | let left_vol = (nr50 & 0b111_0_000) >> 4; 162 | 163 | self.stereo_left_volume = (left_vol as f32) / 7.; 164 | self.stereo_right_volume = (right_vol as f32) / 7.; 165 | } 166 | fn serialise_nr50(&self) -> u8 { 167 | // These might turn out 1 level too low because of float flooring 168 | // TODO: Test this 169 | let right_vol = (self.stereo_right_volume * 7.) as u8; 170 | let left_vol = (self.stereo_left_volume * 7.) as u8; 171 | 172 | (left_vol << 4) & right_vol 173 | } 174 | 175 | pub fn new() -> APU { 176 | APU { 177 | // These might be meant to start 0, not sure 178 | stereo_left_volume: 1., 179 | stereo_right_volume: 1., 180 | stereo_panning: StereoPanning::from(0), 181 | sound_on_register: 0, 182 | 183 | channel1: APUChannel1::new(), 184 | channel2: APUChannel2::new(), 185 | channel3: APUChannel3::new(), 186 | channel4: APUChannel4::new(), 187 | 188 | sample_counter: 0, 189 | buffer: [0; SOUND_BUFFER_SIZE], 190 | buffer_idx: 0, 191 | buffer_full: false, 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /core/src/sound/channel1.rs: -------------------------------------------------------------------------------- 1 | use super::apu::APUChannel; 2 | use super::length_function::LengthFunction; 3 | use super::volume_envelope::VolumeEnvelope; 4 | 5 | const WAVEFORM_TABLE: [u8; 4] = 6 | [0b00000001, 0b00000011, 0b00001111, 0b11111100]; 7 | 8 | // Doing something every 32k cycles is roughly a 128Hz clock. 9 | const SWEEP_CLOCKS: usize = 32_768; 10 | 11 | #[derive(PartialEq)] 12 | enum SweepDirection { 13 | Up, 14 | Down, 15 | } 16 | 17 | // TODO: Refactor to share code with APUChannel2 18 | // They are extremely similar to one another minus the sweep register. 19 | pub struct APUChannel1 { 20 | enabled: bool, 21 | frequency: usize, 22 | frequency_timer: usize, 23 | wave_duty: usize, 24 | wave_duty_position: usize, 25 | volume_envelope: VolumeEnvelope, 26 | length_function: LengthFunction, 27 | shadow_frequency: usize, 28 | shadow_frequency_shift: usize, 29 | sweep_enabled: bool, 30 | sweep_direction: SweepDirection, 31 | sweep_period: usize, 32 | sweep_timer: usize, 33 | sweep_frame_sequencer: usize, 34 | } 35 | 36 | impl APUChannel1 { 37 | pub fn new() -> APUChannel1 { 38 | APUChannel1 { 39 | enabled: false, 40 | frequency: 0, 41 | frequency_timer: 1, 42 | wave_duty: 2, 43 | wave_duty_position: 0, 44 | volume_envelope: VolumeEnvelope::new(), 45 | length_function: LengthFunction::new(), 46 | shadow_frequency: 0, 47 | shadow_frequency_shift: 0, 48 | sweep_enabled: false, 49 | sweep_direction: SweepDirection::Down, 50 | sweep_period: 0, 51 | sweep_timer: 1, 52 | sweep_frame_sequencer: 0, 53 | } 54 | } 55 | 56 | // This is called when a game writes a 1 in bit 7 of the NR24 register. 57 | // That means the game is issuing a "restart sound" command 58 | fn restart_triggered(&mut self) { 59 | self.volume_envelope.restart_triggered(); 60 | self.length_function.restart_triggered(); 61 | self.length_function.channel_enabled = true; 62 | self.enabled = true; 63 | 64 | self.shadow_frequency = self.frequency; 65 | self.sweep_timer = if self.sweep_period == 0 { 66 | 8 67 | } else { 68 | self.sweep_period 69 | }; 70 | if self.sweep_period > 0 || self.shadow_frequency_shift > 0 { 71 | self.sweep_enabled = true; 72 | } 73 | if self.shadow_frequency_shift > 0 { 74 | self.calculate_sweep_frequency(); 75 | } 76 | // TODO: Restarting a tone channel resets its frequency_timer to 77 | // (2048 - frequency) * 4... I think. 78 | } 79 | 80 | fn calculate_sweep_frequency(&mut self) -> usize { 81 | let mut new_frequency = 82 | self.shadow_frequency >> self.shadow_frequency_shift; 83 | 84 | if self.sweep_direction == SweepDirection::Down { 85 | new_frequency = self.shadow_frequency - new_frequency; 86 | } else { 87 | new_frequency = self.shadow_frequency + new_frequency; 88 | } 89 | 90 | if new_frequency > 2047 { 91 | self.enabled = false; 92 | // TODO: Do we nee to cap it here? I'm pretty sure this wasn't a 64-bit 93 | // value on Gameboy. 94 | } 95 | 96 | new_frequency 97 | } 98 | 99 | fn sweep_clock(&mut self) { 100 | if self.sweep_timer > 0 { 101 | self.sweep_timer -= 1; 102 | 103 | if self.sweep_timer == 0 { 104 | self.sweep_timer = if self.sweep_period == 0 { 105 | 8 106 | } else { 107 | self.sweep_period 108 | }; 109 | 110 | if self.sweep_enabled && self.sweep_period != 0 { 111 | let new_frequency = self.calculate_sweep_frequency(); 112 | 113 | if new_frequency <= 2047 && self.shadow_frequency_shift > 0 114 | { 115 | // println!("Setting a new frequency!"); 116 | self.frequency = new_frequency; 117 | self.shadow_frequency = new_frequency; 118 | 119 | self.calculate_sweep_frequency(); 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | impl APUChannel for APUChannel1 { 128 | fn step(&mut self) { 129 | // TODO: I think the Frame Sequencer timers should still be ticking even 130 | // if this channel is not enabled. The Frame Sequencer exists outside 131 | // the channel. 132 | if !self.length_function.channel_enabled { 133 | return; 134 | } 135 | 136 | self.frequency_timer -= 1; 137 | 138 | if self.frequency_timer == 0 { 139 | self.frequency_timer = (2048 - self.frequency) * 4; 140 | 141 | // Wrapping pointer into the bits of the WAVEFORM_TABLE value 142 | self.wave_duty_position += 1; 143 | if self.wave_duty_position == 8 { 144 | self.wave_duty_position = 0 145 | } 146 | } 147 | 148 | self.sweep_frame_sequencer += 1; 149 | if self.sweep_frame_sequencer == SWEEP_CLOCKS { 150 | self.sweep_frame_sequencer = 0; 151 | self.sweep_clock(); 152 | } 153 | 154 | self.volume_envelope.step(); 155 | self.length_function.step(); 156 | } 157 | 158 | fn read(&self, address: u16) -> u8 { 159 | match address { 160 | _ => 0, //panic!("Unimplemented APU Channel 2 read {:#06x}", address) 161 | } 162 | } 163 | 164 | fn write(&mut self, address: u16, value: u8) { 165 | match address { 166 | 0xFF10 => { 167 | // NOTE: This bit is upside down according to Pan Docs. Slightly odd 168 | // hardware quirk. 169 | let sweep_down = (value & 0b0000_1000) > 0; 170 | self.sweep_direction = if sweep_down { 171 | SweepDirection::Down 172 | } else { 173 | SweepDirection::Up 174 | }; 175 | 176 | let sweep_shift = (value & 0b0000_0111) as usize; 177 | self.shadow_frequency_shift = sweep_shift; 178 | 179 | let sweep_period = (value & 0b0111_0000) >> 4; 180 | self.sweep_period = sweep_period as usize; 181 | }, 182 | 0xFF11 => { 183 | let wave_duty = (value & 0b1100_0000) >> 6; 184 | let length = value & 0b0011_1111; 185 | self.wave_duty = wave_duty as usize; 186 | // TODO: Is there a way we change this into a generic register_write 187 | // function for LengthFunction? 188 | self.length_function.data = length as usize; 189 | }, 190 | 0xFF12 => self.volume_envelope.register_write(value), 191 | 0xFF13 => { 192 | // This register sets the bottom 8 bits of the 11-bit 193 | // frequency register. 194 | self.frequency = 195 | (self.frequency & 0b111_0000_0000) | value as usize; 196 | }, 197 | 0xFF14 => { 198 | // Among other things, this register sets the top 3 bits 199 | // of the 11-bit frequency register. 200 | let frequency_bits = value & 0b0000_0111; 201 | self.frequency = (self.frequency & 0b000_1111_1111) 202 | | ((frequency_bits as usize) << 8); 203 | 204 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0; 205 | 206 | if (value & 0b1000_0000) > 0 { 207 | self.restart_triggered(); 208 | } 209 | }, 210 | _ => unreachable!(), 211 | } 212 | } 213 | 214 | fn sample(&self) -> f32 { 215 | if !self.length_function.channel_enabled { 216 | return 0.; 217 | } 218 | if !self.enabled { 219 | return 0.; 220 | } 221 | 222 | let wave_pattern = WAVEFORM_TABLE[self.wave_duty]; 223 | let amplitude_bit = (wave_pattern & (1 << self.wave_duty_position)) 224 | >> self.wave_duty_position; 225 | 226 | let dac_input = amplitude_bit as usize * self.volume_envelope.volume; 227 | // The DAC in the Gameboy outputs between -1.0 and 1.0 228 | (dac_input as f32 / 7.5) - 1.0 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /core/src/sound/channel2.rs: -------------------------------------------------------------------------------- 1 | use super::apu::APUChannel; 2 | use super::length_function::LengthFunction; 3 | use super::volume_envelope::VolumeEnvelope; 4 | 5 | const WAVEFORM_TABLE: [u8; 4] = 6 | [0b00000001, 0b00000011, 0b00001111, 0b11111100]; 7 | 8 | pub struct APUChannel2 { 9 | frequency: usize, 10 | frequency_timer: usize, 11 | wave_duty: usize, 12 | wave_duty_position: usize, 13 | volume_envelope: VolumeEnvelope, 14 | length_function: LengthFunction, 15 | } 16 | 17 | impl APUChannel2 { 18 | pub fn new() -> APUChannel2 { 19 | APUChannel2 { 20 | frequency: 0, 21 | frequency_timer: 1, 22 | wave_duty: 2, 23 | wave_duty_position: 0, 24 | volume_envelope: VolumeEnvelope::new(), 25 | length_function: LengthFunction::new(), 26 | } 27 | } 28 | 29 | // This is called when a game writes a 1 in bit 7 of the NR24 register. 30 | // That means the game is issuing a "restart sound" command 31 | fn restart_triggered(&mut self) { 32 | self.volume_envelope.restart_triggered(); 33 | self.length_function.restart_triggered(); 34 | self.length_function.channel_enabled = true; 35 | // TODO: Restarting a tone channel resets its frequency_timer to 36 | // (2048 - frequency) * 4... I think. 37 | } 38 | } 39 | 40 | impl APUChannel for APUChannel2 { 41 | fn step(&mut self) { 42 | // TODO: I think the Frame Sequencer timers should still be ticking even 43 | // if this channel is not enabled. The Frame Sequencer exists outside 44 | // the channel. 45 | if !self.length_function.channel_enabled { 46 | return; 47 | } 48 | 49 | self.frequency_timer -= 1; 50 | 51 | if self.frequency_timer == 0 { 52 | self.frequency_timer = (2048 - self.frequency) * 4; 53 | 54 | // Wrapping pointer into the bits of the WAVEFORM_TABLE value 55 | self.wave_duty_position += 1; 56 | if self.wave_duty_position == 8 { 57 | self.wave_duty_position = 0 58 | } 59 | } 60 | 61 | self.volume_envelope.step(); 62 | self.length_function.step(); 63 | } 64 | 65 | fn read(&self, address: u16) -> u8 { 66 | match address { 67 | _ => 0, //panic!("Unimplemented APU Channel 2 read {:#06x}", address) 68 | } 69 | } 70 | 71 | fn write(&mut self, address: u16, value: u8) { 72 | match address { 73 | 0xFF16 => { 74 | let wave_duty = (value & 0b1100_0000) >> 6; 75 | let length = value & 0b0011_1111; 76 | self.wave_duty = wave_duty as usize; 77 | // TODO: Is there a way we change this into a generic register_write 78 | // function for LengthFunction? 79 | self.length_function.data = length as usize; 80 | }, 81 | 0xFF17 => self.volume_envelope.register_write(value), 82 | 0xFF18 => { 83 | // This register sets the bottom 8 bits of the 11-bit 84 | // frequency register. 85 | self.frequency = 86 | (self.frequency & 0b111_0000_0000) | value as usize; 87 | }, 88 | 0xFF19 => { 89 | // Among other things, this register sets the top 3 bits 90 | // of the 11-bit frequency register. 91 | let frequency_bits = value & 0b0000_0111; 92 | self.frequency = (self.frequency & 0b000_1111_1111) 93 | | ((frequency_bits as usize) << 8); 94 | 95 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0; 96 | 97 | if (value & 0b1000_0000) > 0 { 98 | self.restart_triggered(); 99 | } 100 | }, 101 | _ => unreachable!(), 102 | } 103 | } 104 | 105 | fn sample(&self) -> f32 { 106 | if !self.length_function.channel_enabled { 107 | return 0.; 108 | } 109 | 110 | let wave_pattern = WAVEFORM_TABLE[self.wave_duty]; 111 | let amplitude_bit = (wave_pattern & (1 << self.wave_duty_position)) 112 | >> self.wave_duty_position; 113 | 114 | let dac_input = amplitude_bit as usize * self.volume_envelope.volume; 115 | // The DAC in the Gameboy outputs between -1.0 and 1.0 116 | (dac_input as f32 / 7.5) - 1.0 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /core/src/sound/channel3.rs: -------------------------------------------------------------------------------- 1 | use super::apu::APUChannel; 2 | use super::length_function::LengthFunction; 3 | use crate::constants::*; 4 | use crate::memory::ram::Ram; 5 | 6 | pub struct APUChannel3 { 7 | frequency: usize, 8 | frequency_timer: usize, 9 | // TODO: Is this actually the same bool as the enable within length_function? 10 | master_enable: bool, 11 | length_function: LengthFunction, 12 | wave_ram: Ram, 13 | wave_ram_ptr: usize, 14 | volume_shift: u8, 15 | } 16 | 17 | impl APUChannel3 { 18 | pub fn new() -> APUChannel3 { 19 | APUChannel3 { 20 | frequency: 0, 21 | frequency_timer: 1, 22 | master_enable: false, 23 | length_function: LengthFunction::new(), 24 | wave_ram: Ram::new(WAVE_RAM_SIZE), 25 | wave_ram_ptr: 0, 26 | volume_shift: 0, 27 | } 28 | } 29 | 30 | fn restart_triggered(&mut self) { 31 | self.length_function.restart_triggered(); 32 | self.length_function.channel_enabled = true; 33 | self.wave_ram_ptr = 0; 34 | } 35 | } 36 | 37 | impl APUChannel for APUChannel3 { 38 | fn step(&mut self) { 39 | if !self.length_function.channel_enabled { 40 | return; 41 | } 42 | 43 | self.frequency_timer -= 1; 44 | 45 | if self.frequency_timer == 0 { 46 | self.frequency_timer = (2048 - self.frequency) * 2; 47 | 48 | // NOTE: This points to a *nibble* of Wave RAM, not a byte. 49 | self.wave_ram_ptr += 1; 50 | if self.wave_ram_ptr == 32 { 51 | self.wave_ram_ptr = 0; 52 | } 53 | } 54 | 55 | self.length_function.step(); 56 | } 57 | 58 | fn read(&self, address: u16) -> u8 { 59 | match address { 60 | 0xFF1A => (self.master_enable as u8) >> 7, 61 | WAVE_RAM_START..=WAVE_RAM_END => { 62 | self.wave_ram.read(address - WAVE_RAM_START) 63 | }, 64 | _ => 0, //panic!("Unimplemented APU Channel 3 read {:#06x}", address) 65 | } 66 | } 67 | 68 | fn write(&mut self, address: u16, value: u8) { 69 | match address { 70 | 0xFF1A => { 71 | self.master_enable = (value & 0b1000_0000) > 0; 72 | }, 73 | 0xFF1B => { 74 | self.length_function.data = value as usize; 75 | }, 76 | 0xFF1C => { 77 | self.volume_shift = (value & 0b0110_0000) >> 5; 78 | }, 79 | 0xFF1D => { 80 | self.frequency = 81 | (self.frequency & 0b111_0000_0000) | value as usize; 82 | }, 83 | 0xFF1E => { 84 | let frequency_bits = value & 0b0000_0111; 85 | self.frequency = (self.frequency & 0b000_1111_1111) 86 | | ((frequency_bits as usize) << 8); 87 | 88 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0; 89 | 90 | if (value & 0b1000_0000) > 0 { 91 | self.restart_triggered(); 92 | } 93 | }, 94 | WAVE_RAM_START..=WAVE_RAM_END => { 95 | self.wave_ram.write(address - WAVE_RAM_START, value) 96 | }, 97 | _ => unreachable!(), 98 | } 99 | } 100 | 101 | fn sample(&self) -> f32 { 102 | if !self.length_function.channel_enabled { 103 | return 0.; 104 | } 105 | 106 | // This implementation is a bit guessed for now :) 107 | // Documentation on Channel 3 seems a little bit thin 108 | let wave_byte = self.wave_ram.bytes[self.wave_ram_ptr / 2]; 109 | let mut wave_nibble = if self.wave_ram_ptr % 2 == 0 { 110 | wave_byte & 0x0F 111 | } else { 112 | wave_byte >> 4 113 | }; 114 | 115 | wave_nibble = wave_nibble 116 | >> match self.volume_shift { 117 | 0 => 4, 118 | 1 => 0, 119 | 2 => 1, 120 | 3 => 2, 121 | _ => unreachable!(), 122 | }; 123 | 124 | (wave_nibble as f32 / 7.5) - 1.0 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /core/src/sound/channel4.rs: -------------------------------------------------------------------------------- 1 | use super::apu::APUChannel; 2 | use super::length_function::LengthFunction; 3 | use super::volume_envelope::VolumeEnvelope; 4 | 5 | pub struct APUChannel4 { 6 | // TODO: Size these better. Maybe u32 rather than usize? 7 | // Not super important at all, but just to be sure. 8 | frequency_timer: usize, 9 | length_function: LengthFunction, 10 | volume_envelope: VolumeEnvelope, 11 | // Linear Feedback Shift Register 12 | // NOTE: The LFSR is 15-bits wide on the Gameboy. We'll use a 16-bit type 13 | // to represent it. 14 | lfsr: u16, 15 | // TODO: Is this a u8? Does shifting large-ish numbers overflow? 16 | divisor_shift: usize, 17 | half_width_mode: bool, 18 | divisor_code: usize, 19 | } 20 | 21 | impl APUChannel4 { 22 | pub fn new() -> APUChannel4 { 23 | APUChannel4 { 24 | frequency_timer: 1, 25 | length_function: LengthFunction::new(), 26 | volume_envelope: VolumeEnvelope::new(), 27 | lfsr: 0, 28 | divisor_shift: 0, 29 | half_width_mode: false, 30 | divisor_code: 0, 31 | } 32 | } 33 | 34 | fn restart_triggered(&mut self) { 35 | self.length_function.restart_triggered(); 36 | self.volume_envelope.restart_triggered(); 37 | self.lfsr = 0b0111_1111_1111_1111; 38 | } 39 | 40 | fn get_divisor(&self) -> usize { 41 | match self.divisor_code { 42 | 0 => 8, 43 | 1 => 16, 44 | 2 => 32, 45 | 3 => 48, 46 | 4 => 64, 47 | 5 => 80, 48 | 6 => 96, 49 | 7 => 112, 50 | _ => unreachable!(), 51 | } 52 | } 53 | } 54 | 55 | impl APUChannel for APUChannel4 { 56 | fn step(&mut self) { 57 | if !self.length_function.channel_enabled { 58 | return; 59 | } 60 | 61 | self.frequency_timer -= 1; 62 | 63 | if self.frequency_timer == 0 { 64 | self.frequency_timer = self.get_divisor() << self.divisor_shift; 65 | 66 | // Pseudo-random white noise generation 67 | let xor = (self.lfsr & 0b01) ^ ((self.lfsr & 0b10) >> 1); 68 | self.lfsr = (self.lfsr >> 1) | (xor << 14); 69 | 70 | if self.half_width_mode { 71 | self.lfsr = (self.lfsr & 0b0011_1111) | (xor << 6); 72 | } 73 | } 74 | 75 | self.volume_envelope.step(); 76 | self.length_function.step(); 77 | } 78 | 79 | fn read(&self, address: u16) -> u8 { 80 | match address { 81 | _ => 0, //panic!("Unimplemented APU Channel 4 read {:#06x}", address) 82 | } 83 | } 84 | 85 | fn write(&mut self, address: u16, value: u8) { 86 | match address { 87 | 0xFF20 => { 88 | let length = value & 0b0011_1111; 89 | self.length_function.data = length as usize; 90 | }, 91 | 0xFF21 => self.volume_envelope.register_write(value), 92 | 0xFF22 => { 93 | // Polynomial register 94 | self.divisor_shift = ((value & 0b1111_0000) >> 4) as usize; 95 | self.half_width_mode = (value & 0b0000_1000) > 0; 96 | self.divisor_code = (value & 0b0000_0111) as usize; 97 | }, 98 | 0xFF23 => { 99 | self.length_function.timer_enabled = (value & 0b0100_0000) > 0; 100 | 101 | if (value & 0b1000_0000) > 0 { 102 | self.restart_triggered(); 103 | } 104 | }, 105 | _ => unreachable!(), 106 | } 107 | } 108 | 109 | fn sample(&self) -> f32 { 110 | if !self.length_function.channel_enabled { 111 | return 0.; 112 | } 113 | 114 | let lfsr_bit = !(self.lfsr) & 1; 115 | 116 | let dac_input = lfsr_bit as usize * self.volume_envelope.volume; 117 | (dac_input as f32 / 7.5) - 1.0 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /core/src/sound/length_function.rs: -------------------------------------------------------------------------------- 1 | // 256Hz 2 | const LENGTH_CLOCKS: usize = 16_392; 3 | 4 | pub struct LengthFunction { 5 | pub channel_enabled: bool, 6 | pub data: usize, 7 | pub timer_enabled: bool, 8 | timer: usize, 9 | clock_timer: usize, 10 | } 11 | 12 | impl LengthFunction { 13 | pub fn step(&mut self) { 14 | self.clock_timer += 1; 15 | if self.clock_timer == LENGTH_CLOCKS { 16 | self.clock_timer = 0; 17 | self.clock(); 18 | } 19 | } 20 | 21 | pub fn restart_triggered(&mut self) { 22 | // TODO: This behaviour isn't quite right 23 | // https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Trigger_Event 24 | self.timer = 64; 25 | self.timer = self.timer.saturating_sub(self.data); 26 | } 27 | 28 | // Called at 256Hz 29 | fn clock(&mut self) { 30 | if self.timer > 0 { 31 | self.timer -= 1; 32 | } 33 | 34 | if self.timer == 0 { 35 | if self.timer_enabled { 36 | // TODO: Without saturating_sub, this causes panics after moving to 37 | // 48KHz audio when flapping with the rabbit ears in Mario Land 2 38 | self.timer = 64; 39 | self.timer = self.timer.saturating_sub(self.data); 40 | self.channel_enabled = false; 41 | } 42 | } 43 | } 44 | 45 | pub fn new() -> LengthFunction { 46 | LengthFunction { 47 | channel_enabled: true, 48 | timer: 0, 49 | data: 0, 50 | timer_enabled: false, 51 | clock_timer: 0, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/sound/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apu; 2 | pub mod channel1; 3 | pub mod channel2; 4 | pub mod channel3; 5 | pub mod channel4; 6 | pub mod length_function; 7 | pub mod registers; 8 | pub mod volume_envelope; 9 | -------------------------------------------------------------------------------- /core/src/sound/registers.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone)] 2 | pub struct StereoPanning { 3 | pub channel1_left: bool, 4 | pub channel1_right: bool, 5 | pub channel2_left: bool, 6 | pub channel2_right: bool, 7 | pub channel3_left: bool, 8 | pub channel3_right: bool, 9 | pub channel4_left: bool, 10 | pub channel4_right: bool, 11 | } 12 | 13 | impl From for StereoPanning { 14 | fn from(n: u8) -> StereoPanning { 15 | StereoPanning { 16 | channel4_left: (n & 0b1000_0000) > 0, 17 | channel3_left: (n & 0b0100_0000) > 0, 18 | channel2_left: (n & 0b0010_0000) > 0, 19 | channel1_left: (n & 0b0001_0000) > 0, 20 | channel4_right: (n & 0b0000_1000) > 0, 21 | channel3_right: (n & 0b0000_0100) > 0, 22 | channel2_right: (n & 0b0000_0010) > 0, 23 | channel1_right: (n & 0b0000_0001) > 0, 24 | } 25 | } 26 | } 27 | 28 | impl From for u8 { 29 | fn from(stereo: StereoPanning) -> u8 { 30 | (stereo.channel1_right as u8) 31 | | ((stereo.channel2_right as u8) << 1) 32 | | ((stereo.channel3_right as u8) << 2) 33 | | ((stereo.channel4_right as u8) << 3) 34 | | ((stereo.channel1_left as u8) << 4) 35 | | ((stereo.channel2_left as u8) << 5) 36 | | ((stereo.channel3_left as u8) << 6) 37 | | ((stereo.channel4_left as u8) << 7) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/sound/volume_envelope.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq)] 2 | enum EnvelopeDirection { 3 | Up, 4 | Down, 5 | } 6 | 7 | // Doing something every 65k cycles is roughly a 64Hz clock. 8 | const ENV_CLOCKS: usize = 65_536; 9 | 10 | pub struct VolumeEnvelope { 11 | initial_volume: usize, 12 | direction: EnvelopeDirection, 13 | sweep_period: usize, 14 | period_timer: usize, 15 | volume_timer: usize, 16 | pub volume: usize, 17 | } 18 | 19 | impl VolumeEnvelope { 20 | pub fn step(&mut self) { 21 | self.volume_timer += 1; 22 | if self.volume_timer == ENV_CLOCKS { 23 | self.volume_timer = 0; 24 | self.clock(); 25 | } 26 | } 27 | 28 | pub fn restart_triggered(&mut self) { 29 | self.period_timer = self.sweep_period; 30 | self.volume = self.initial_volume; 31 | } 32 | 33 | pub fn register_write(&mut self, value: u8) { 34 | self.initial_volume = (value as usize & 0b1111_0000) >> 4; 35 | self.direction = if (value & 0b0000_1000) > 0 { 36 | EnvelopeDirection::Up 37 | } else { 38 | EnvelopeDirection::Down 39 | }; 40 | self.sweep_period = value as usize & 0b0000_0111; 41 | } 42 | 43 | // Called at 64Hz 44 | fn clock(&mut self) { 45 | if self.sweep_period == 0 { 46 | return; 47 | } 48 | 49 | if self.period_timer > 0 { 50 | self.period_timer -= 1; 51 | } 52 | 53 | if self.period_timer == 0 { 54 | self.period_timer = self.sweep_period; 55 | 56 | if self.direction == EnvelopeDirection::Up && self.volume < 0xF { 57 | self.volume += 1; 58 | } 59 | if self.direction == EnvelopeDirection::Down && self.volume > 0 { 60 | self.volume -= 1; 61 | } 62 | } 63 | } 64 | 65 | pub fn new() -> VolumeEnvelope { 66 | VolumeEnvelope { 67 | initial_volume: 0, 68 | direction: EnvelopeDirection::Down, 69 | sweep_period: 0, 70 | period_timer: 0, 71 | volume: 0, 72 | volume_timer: 0, 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /libretro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gbrs-libretro" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | libretro-rs = { git = "https://github.com/libretro-rs/libretro-rs.git", features = [ 8 | "experimental", 9 | ] } 10 | gbrs-core = { path = "../core", features = ["sound"], default-features = false } 11 | spin = { version = "0.9.8", features = ["once", "spin_mutex"] } 12 | 13 | [lib] 14 | crate-type = ["cdylib"] 15 | -------------------------------------------------------------------------------- /libretro/libgbrs_libretro.info: -------------------------------------------------------------------------------- 1 | # Software Information 2 | display_name = "Nintendo - Game Boy / Color (gbrs)" 3 | authors = "Adam Soutar" 4 | supported_extensions = "gb|gbc" 5 | corename = "gbrs" 6 | display_version = "0.1.0" 7 | categories = "Emulator" 8 | license = "MIT" 9 | permissions = "" 10 | 11 | # Hardware Information 12 | manufacturer = "Nintendo" 13 | systemname = "Game Boy/Game Boy Color" 14 | systemid = "game_boy" 15 | 16 | # Libretro Features 17 | database = "Nintendo - Game Boy|Nintendo - Game Boy Color" 18 | supports_no_game = "false" 19 | 20 | description = "A programmer's hobby project. Decently accurate in DMG games, but not a replacement for a 'proper' emulator." 21 | -------------------------------------------------------------------------------- /libretro/run.sh: -------------------------------------------------------------------------------- 1 | cargo build && 2 | RUST_BACKTRACE=1 /Applications/RetroArch.app/Contents/MacOS/RetroArch --verbose -L ./target/debug/libgbrs_libretro.dylib "../roms/$1.gb"" 3 | -------------------------------------------------------------------------------- /libretro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gbrs_core::config::Config; 2 | use gbrs_core::constants::*; 3 | use gbrs_core::cpu::Cpu; 4 | use gbrs_core::memory::rom::Rom; 5 | use libretro_rs::c_utf8::{c_utf8, CUtf8}; 6 | use libretro_rs::ffi::retro_log_level::*; 7 | use libretro_rs::retro::env::{Init, UnloadGame}; 8 | use libretro_rs::retro::pixel::{Format, XRGB8888}; 9 | use libretro_rs::retro::*; 10 | use libretro_rs::{ext, libretro_core}; 11 | use spin::{mutex::SpinMutex, Once}; 12 | 13 | struct LibretroCore { 14 | gameboy: Cpu, 15 | last_cpu_config: Config, 16 | rendering_mode: SoftwareRenderEnabled, 17 | frame_buffer: [XRGB8888; SCREEN_WIDTH * SCREEN_HEIGHT], 18 | pixel_format: Format, 19 | } 20 | 21 | static LOGGER: Once> = Once::new(); 22 | 23 | impl<'a> Core<'a> for LibretroCore { 24 | type Init = (); 25 | 26 | fn get_system_info() -> SystemInfo { 27 | SystemInfo::new( 28 | c_utf8!("gbrs"), 29 | c_utf8!(env!("CARGO_PKG_VERSION")), 30 | ext!["gb", "gbc"], 31 | ) 32 | } 33 | 34 | fn init(env: &mut impl Init) -> Self::Init { 35 | LOGGER.call_once(|| SpinMutex::new(env.get_log_interface().unwrap())); 36 | 37 | gbrs_core::callbacks::set_callbacks(gbrs_core::callbacks::Callbacks { 38 | log: |log_str| { 39 | let null_terminated = &format!("{}\0", log_str)[..]; 40 | let retro_str = CUtf8::from_str(null_terminated).unwrap(); 41 | if let Some(logger) = LOGGER.get() { 42 | logger.lock().log(RETRO_LOG_INFO, retro_str); 43 | } 44 | }, 45 | save: |_game_name, _rom_path, _save_data| {}, 46 | load: |_game_name, _rom_path, expected_size| vec![0; expected_size], 47 | }) 48 | } 49 | 50 | fn get_system_av_info( 51 | &self, 52 | _env: &mut impl env::GetAvInfo, 53 | ) -> SystemAVInfo { 54 | SystemAVInfo::new( 55 | GameGeometry::fixed(SCREEN_WIDTH as u16, SCREEN_HEIGHT as u16), 56 | SystemTiming::new( 57 | DEFAULT_FRAME_RATE as f64, 58 | SOUND_SAMPLE_RATE as f64, 59 | ), 60 | ) 61 | } 62 | 63 | fn run( 64 | &mut self, 65 | _env: &mut impl env::Run, 66 | runtime: &mut impl Callbacks, 67 | ) -> InputsPolled { 68 | let gb = &mut self.gameboy; 69 | 70 | for i in 0..SCREEN_BUFFER_SIZE { 71 | let mut pixel: u32 = 0; 72 | let r = gb.gpu.finished_frame[i].red; 73 | let g = gb.gpu.finished_frame[i].green; 74 | let b = gb.gpu.finished_frame[i].blue; 75 | pixel |= b as u32; 76 | pixel |= (g as u32) << 8; 77 | pixel |= (r as u32) << 16; 78 | self.frame_buffer[i] = XRGB8888::new_with_raw_value(pixel); 79 | } 80 | 81 | let frame = Frame::new( 82 | &self.frame_buffer, 83 | SCREEN_WIDTH as u32, 84 | SCREEN_HEIGHT as u32, 85 | ); 86 | runtime.upload_video_frame( 87 | &self.rendering_mode, 88 | &self.pixel_format, 89 | &frame, 90 | ); 91 | runtime.upload_audio_frame(&gb.mem.apu.buffer); 92 | 93 | let inputs_polled = runtime.poll_inputs(); 94 | let port = DevicePort::new(0); 95 | gb.mem.joypad.a_pressed = 96 | runtime.is_joypad_button_pressed(port, JoypadButton::A); 97 | gb.mem.joypad.b_pressed = 98 | runtime.is_joypad_button_pressed(port, JoypadButton::B); 99 | gb.mem.joypad.start_pressed = 100 | runtime.is_joypad_button_pressed(port, JoypadButton::Start); 101 | gb.mem.joypad.select_pressed = 102 | runtime.is_joypad_button_pressed(port, JoypadButton::Select); 103 | gb.mem.joypad.left_pressed = 104 | runtime.is_joypad_button_pressed(port, JoypadButton::Left); 105 | gb.mem.joypad.right_pressed = 106 | runtime.is_joypad_button_pressed(port, JoypadButton::Right); 107 | gb.mem.joypad.up_pressed = 108 | runtime.is_joypad_button_pressed(port, JoypadButton::Up); 109 | gb.mem.joypad.down_pressed = 110 | runtime.is_joypad_button_pressed(port, JoypadButton::Down); 111 | 112 | while !gb.mem.apu.buffer_full { 113 | gb.step(); 114 | } 115 | gb.mem.apu.buffer_full = false; 116 | 117 | inputs_polled 118 | } 119 | 120 | fn load_game( 121 | game: &GameInfo, 122 | args: LoadGameExtraArgs<'a, '_, E, Self::Init>, 123 | ) -> Result { 124 | let LoadGameExtraArgs { 125 | env, 126 | pixel_format, 127 | rendering_mode, 128 | .. 129 | } = args; 130 | let pixel_format = env.set_pixel_format_xrgb8888(pixel_format)?; 131 | let data: &[u8] = game.as_data().ok_or(CoreError::new())?.data(); 132 | let config = Config { 133 | sound_buffer_size: SOUND_BUFFER_SIZE, 134 | sound_sample_rate: SOUND_SAMPLE_RATE, 135 | rom: Rom::from_bytes(data.to_vec()), 136 | }; 137 | Ok(Self { 138 | rendering_mode, 139 | pixel_format, 140 | gameboy: Cpu::from_config(config.clone()), 141 | last_cpu_config: config, 142 | frame_buffer: [XRGB8888::DEFAULT; SCREEN_WIDTH * SCREEN_HEIGHT], 143 | }) 144 | } 145 | 146 | fn reset(&mut self, _env: &mut impl env::Reset) { 147 | self.gameboy = Cpu::from_config(self.last_cpu_config.clone()); 148 | } 149 | 150 | fn unload_game(self, _env: &mut impl UnloadGame) -> Self::Init { 151 | () 152 | } 153 | } 154 | 155 | libretro_core!(crate::LibretroCore); 156 | -------------------------------------------------------------------------------- /profiling/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "profiling" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [profile.release] 7 | panic = "abort" 8 | opt-level = 3 9 | lto = false 10 | debug = true 11 | split-debuginfo = "packed" 12 | 13 | [dependencies] 14 | gbrs-core = { path = "../core", default-features = false, features = ["std"] } 15 | -------------------------------------------------------------------------------- /profiling/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::time::SystemTime; 3 | 4 | use gbrs_core::{ 5 | config::Config, 6 | constants::{SOUND_BUFFER_SIZE, SOUND_SAMPLE_RATE}, 7 | cpu::Cpu, 8 | memory::rom::Rom, 9 | }; 10 | 11 | const RUNS: u128 = 5000; 12 | 13 | fn main() { 14 | let rom_path = env::args().nth(1).expect("Pass a ROM path as an argument"); 15 | let mut processor = Cpu::from_config(Config { 16 | rom: Rom::from_file(&rom_path), 17 | sound_buffer_size: SOUND_BUFFER_SIZE, 18 | sound_sample_rate: SOUND_SAMPLE_RATE, 19 | }); 20 | 21 | // Just run the CPU forever so we can profile hot areas of emulation. 22 | let mut harness_total = 0; 23 | for _ in 0..RUNS { 24 | let now = SystemTime::now(); 25 | 26 | processor.step_one_frame(); 27 | 28 | let time = now.elapsed().unwrap().as_micros(); 29 | harness_total += time; 30 | } 31 | 32 | println!( 33 | "Average execution time across {} runs: {} microseconds", 34 | RUNS, 35 | harness_total / RUNS 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /roms/DMG-ACID2-LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matt Currie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /roms/dmg-acid2.gb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamsoutar/gbrs/1a019087a797af467196ecebb47447ac5bd6c527/roms/dmg-acid2.gb -------------------------------------------------------------------------------- /sdl-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gbrs-sdl-gui" 3 | version = "0.2.0" 4 | authors = ["Adam Soutar "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | gbrs-core = { path = "../core" } 9 | sdl2 = { version = "0.37.0", features = ["bundled"] } 10 | -------------------------------------------------------------------------------- /sdl-gui/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(target_os = "macos")] 3 | println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path"); 4 | 5 | #[cfg(target_os = "linux")] 6 | println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"); 7 | } 8 | -------------------------------------------------------------------------------- /sdl-gui/src/gui.rs: -------------------------------------------------------------------------------- 1 | use gbrs_core::constants::*; 2 | use gbrs_core::cpu::Cpu; 3 | 4 | use sdl2::audio::{AudioQueue, AudioSpecDesired}; 5 | use sdl2::event::Event; 6 | use sdl2::keyboard::Scancode; 7 | use sdl2::pixels::Color; 8 | use sdl2::rect::Rect; 9 | 10 | // NOTE: The SDL port does not currently perform non-integer scaling. 11 | // Please choose a multiple of 160x144 12 | const WINDOW_WIDTH: u32 = 800; 13 | const WINDOW_HEIGHT: u32 = 720; 14 | 15 | pub fn run_gui(mut gameboy: Cpu) { 16 | let sdl_context = sdl2::init().unwrap(); 17 | let video_subsystem = sdl_context.video().unwrap(); 18 | 19 | let window_title = format!("{} - gbrs (SDL)", gameboy.cart_info.title); 20 | let window = video_subsystem 21 | .window(&window_title[..], WINDOW_WIDTH, WINDOW_HEIGHT) 22 | .position_centered() 23 | .build() 24 | .unwrap(); 25 | 26 | let square_width = WINDOW_WIDTH as usize / SCREEN_WIDTH; 27 | let square_height = WINDOW_HEIGHT as usize / SCREEN_HEIGHT; 28 | 29 | let mut canvas = window 30 | .into_canvas() 31 | // TODO: This option fixes visual tearing, but it messes up our sound 32 | // timing code, and the speed of the emulator is thrown way off :( 33 | // .present_vsync() 34 | .build() 35 | .unwrap(); 36 | 37 | canvas.set_draw_color(Color::RGB(255, 255, 255)); 38 | canvas.clear(); 39 | canvas.present(); 40 | let mut event_pump = sdl_context.event_pump().unwrap(); 41 | 42 | let audio_subsystem = sdl_context.audio().unwrap(); 43 | let desired_spec = AudioSpecDesired { 44 | freq: Some(SOUND_SAMPLE_RATE as i32), 45 | channels: Some(2), 46 | samples: Some(SOUND_BUFFER_SIZE as u16), 47 | }; 48 | 49 | let audio_queue: AudioQueue = 50 | audio_subsystem.open_queue(None, &desired_spec).unwrap(); 51 | 52 | assert_eq!( 53 | audio_queue.spec().samples, 54 | SOUND_BUFFER_SIZE as u16, 55 | "Audio device does not support gbrs' sound buffer size" 56 | ); 57 | 58 | gameboy.step_until_full_audio_buffer(); 59 | // gameboy.mem.apu.buffer_full = true; 60 | 61 | 'running: loop { 62 | for event in event_pump.poll_iter() { 63 | if matches!(event, Event::Quit { .. }) { 64 | break 'running; 65 | } 66 | } 67 | 68 | // Draw the screen 69 | for x in 0..SCREEN_WIDTH { 70 | for y in 0..SCREEN_HEIGHT { 71 | let i = (y * 160 + x) as usize; 72 | let colour = &gameboy.gpu.finished_frame[i]; 73 | canvas.set_draw_color(Color::RGB( 74 | colour.red, 75 | colour.green, 76 | colour.blue, 77 | )); 78 | canvas 79 | .fill_rect(Rect::new( 80 | (x * square_width) as i32, 81 | (y * square_height) as i32, 82 | square_width as u32, 83 | square_height as u32, 84 | )) 85 | .unwrap(); 86 | } 87 | } 88 | canvas.present(); 89 | 90 | gameboy.mem.joypad.start_pressed = event_pump 91 | .keyboard_state() 92 | .is_scancode_pressed(Scancode::Return); 93 | gameboy.mem.joypad.select_pressed = event_pump 94 | .keyboard_state() 95 | .is_scancode_pressed(Scancode::Backspace); 96 | gameboy.mem.joypad.a_pressed = 97 | event_pump.keyboard_state().is_scancode_pressed(Scancode::X); 98 | gameboy.mem.joypad.b_pressed = 99 | event_pump.keyboard_state().is_scancode_pressed(Scancode::Z); 100 | gameboy.mem.joypad.left_pressed = event_pump 101 | .keyboard_state() 102 | .is_scancode_pressed(Scancode::Left); 103 | gameboy.mem.joypad.right_pressed = event_pump 104 | .keyboard_state() 105 | .is_scancode_pressed(Scancode::Right); 106 | gameboy.mem.joypad.up_pressed = event_pump 107 | .keyboard_state() 108 | .is_scancode_pressed(Scancode::Up); 109 | gameboy.mem.joypad.down_pressed = event_pump 110 | .keyboard_state() 111 | .is_scancode_pressed(Scancode::Down); 112 | 113 | gameboy.step_until_full_audio_buffer(); 114 | 115 | let pre = audio_queue.size(); 116 | audio_queue.queue_audio(&gameboy.mem.apu.buffer).unwrap(); 117 | audio_queue.resume(); 118 | let diff = audio_queue.size() - pre; 119 | 120 | while audio_queue.size() > diff { 121 | // NOTE: You can comment this if statement out if you're having 122 | // speed or sound issues. It is an attempt to help out slower 123 | // machines, but you may not need it if your machine is fast 124 | // enough. 125 | if !gameboy.mem.apu.buffer_full { 126 | gameboy.step(); 127 | } 128 | std::hint::spin_loop(); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /sdl-gui/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod gui; 2 | 3 | use std::env; 4 | 5 | use gbrs_core::config::Config; 6 | use gbrs_core::cpu::Cpu; 7 | use gbrs_core::memory::rom::Rom; 8 | use gui::run_gui; 9 | 10 | // TODO: Get these from an SDL audio device 11 | const SOUND_BUFFER_SIZE: usize = 1024; 12 | const SOUND_SAMPLE_RATE: usize = 48000; 13 | 14 | fn main() { 15 | let rom_path = env::args().nth(1).expect("Pass a ROM path as an argument"); 16 | let processor = Cpu::from_config(Config { 17 | sound_buffer_size: SOUND_BUFFER_SIZE, 18 | sound_sample_rate: SOUND_SAMPLE_RATE, 19 | rom: Rom::from_file(&rom_path), 20 | }); 21 | run_gui(processor); 22 | } 23 | -------------------------------------------------------------------------------- /sfml-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gbrs-sfml-gui" 3 | version = "0.2.0" 4 | authors = ["Adam Soutar "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | gbrs-core = { path = "../core" } 9 | sfml = "0.24.0" 10 | spin = { version = "0.9.8", features = ["spin_mutex"] } 11 | -------------------------------------------------------------------------------- /sfml-gui/src/control.rs: -------------------------------------------------------------------------------- 1 | use gbrs_core::cpu::Cpu; 2 | 3 | use sfml::window::joystick::*; 4 | use sfml::window::*; 5 | 6 | // TODO: Mappings for: 7 | // - Xbox 360 8 | // - Xbox One 9 | // - DualShock 5 10 | // - The above on Windows 11 | #[allow(dead_code)] 12 | mod ps4 { 13 | // These are ascertained through experimentation with a wired DualShock 4 14 | // on macOS. 15 | use sfml::window::joystick::Axis; 16 | 17 | pub const X: u32 = 1; 18 | pub const SQUARE: u32 = 0; 19 | pub const TRIANGLE: u32 = 3; 20 | pub const CIRCLE: u32 = 2; 21 | 22 | pub const START: u32 = 9; 23 | pub const SHARE: u32 = 8; 24 | pub const TOUCHPAD: u32 = 13; 25 | 26 | pub const LEFT_STICK_X: Axis = Axis::X; 27 | pub const LEFT_STICK_Y: Axis = Axis::Y; 28 | pub const RIGHT_STICK_X: Axis = Axis::Z; 29 | pub const RIGHT_STICK_Y: Axis = Axis::R; 30 | pub const DPAD_X: Axis = Axis::PovX; 31 | pub const DPAD_Y: Axis = Axis::PovY; 32 | 33 | // DualShock 4 axes go from -100 to +100 34 | pub const DEADZONE: f32 = 25.; 35 | } 36 | 37 | pub fn update_joypad_state(gameboy: &mut Cpu) { 38 | // TODO: Raise the joypad interrupt 39 | gameboy.mem.joypad.a_pressed = 40 | key(Key::X) || joy(ps4::X) || joy(ps4::CIRCLE); 41 | 42 | gameboy.mem.joypad.b_pressed = 43 | key(Key::Z) || joy(ps4::SQUARE) || joy(ps4::TRIANGLE); 44 | 45 | gameboy.mem.joypad.start_pressed = key(Key::Enter) || joy(ps4::START); 46 | 47 | gameboy.mem.joypad.select_pressed = 48 | key(Key::Backspace) || joy(ps4::TOUCHPAD) || joy(ps4::SHARE); 49 | 50 | gameboy.mem.joypad.up_pressed = key(Key::Up) 51 | || axis(ps4::LEFT_STICK_Y, false) 52 | || axis(ps4::DPAD_Y, true); 53 | 54 | gameboy.mem.joypad.down_pressed = key(Key::Down) 55 | || axis(ps4::LEFT_STICK_Y, true) 56 | || axis(ps4::DPAD_Y, false); 57 | 58 | gameboy.mem.joypad.left_pressed = key(Key::Left) 59 | || axis(ps4::LEFT_STICK_X, false) 60 | || axis(ps4::DPAD_X, false); 61 | 62 | gameboy.mem.joypad.right_pressed = key(Key::Right) 63 | || axis(ps4::LEFT_STICK_X, true) 64 | || axis(ps4::DPAD_X, true); 65 | } 66 | 67 | fn key(key: Key) -> bool { 68 | Key::is_pressed(key) 69 | } 70 | fn joy(button: u32) -> bool { 71 | for i in 0..joystick::COUNT { 72 | if joystick::is_button_pressed(i, button) { 73 | return true; 74 | } 75 | } 76 | 77 | false 78 | } 79 | fn axis(target: Axis, positive: bool) -> bool { 80 | for i in 0..joystick::COUNT { 81 | let mut val = joystick::axis_position(i, target); 82 | if !positive { 83 | val = -val 84 | } 85 | 86 | if val > 100. - ps4::DEADZONE { 87 | return true; 88 | } 89 | } 90 | 91 | false 92 | } 93 | -------------------------------------------------------------------------------- /sfml-gui/src/gui.rs: -------------------------------------------------------------------------------- 1 | use crate::control::*; 2 | 3 | use gbrs_core::constants::*; 4 | use gbrs_core::cpu::Cpu; 5 | 6 | use sfml::audio::{Sound, SoundBuffer, SoundStatus}; 7 | use sfml::graphics::*; 8 | use sfml::system::*; 9 | use sfml::window::*; 10 | use spin::mutex::SpinMutex; 11 | 12 | pub const STEP_BY_STEP: bool = false; 13 | // NOTE: This debug option is only supported on macOS. See note below 14 | pub const DRAW_FPS: bool = false; 15 | 16 | static SOUND_BACKING_STORE: SpinMutex<[i16; SOUND_BUFFER_SIZE]> = 17 | SpinMutex::new([0; SOUND_BUFFER_SIZE]); 18 | 19 | pub fn run_gui(mut gameboy: Cpu) { 20 | let sw = SCREEN_WIDTH as u32; 21 | let sh = SCREEN_HEIGHT as u32; 22 | let window_width: u32 = 640; 23 | let window_height: u32 = 512; 24 | 25 | let style = Style::RESIZE | Style::TITLEBAR | Style::CLOSE; 26 | let mut window = RenderWindow::new( 27 | (window_width, window_height), 28 | &format!("{} - gbrs (SFML)", gameboy.cart_info.title)[..], 29 | style, 30 | &ContextSettings::default(), 31 | ) 32 | .unwrap(); 33 | // window.set_framerate_limit(gameboy.frame_rate as u32); 34 | 35 | let mut screen_texture = Texture::new().unwrap(); 36 | screen_texture 37 | .create(sw, sh) 38 | .expect("Failed to create screen texture"); 39 | 40 | // Scale the 160x144 image to the appropriate resolution 41 | let sprite_scale = Vector2f::new( 42 | window_width as f32 / sw as f32, 43 | window_height as f32 / sh as f32, 44 | ); 45 | 46 | let mut clock = Clock::start().unwrap(); 47 | 48 | let font; 49 | let mut text = None; 50 | if DRAW_FPS { 51 | // NOTE: DRAW_FPS only works on macOS at the moment due to hardcoded 52 | // font paths. I don't want to include a font in the gbrs repo just 53 | // for this debug feature. 54 | font = Font::from_file("/System/Library/Fonts/Menlo.ttc").unwrap(); 55 | text = Some(Text::new("", &font, 32)); 56 | // Make it stick out instead of white on a black+white screen 57 | text.as_mut().unwrap().set_fill_color(Color::BLUE); 58 | } 59 | 60 | // Get the initial frame & buffer of audio 61 | gameboy.step_until_full_audio_buffer(); 62 | 63 | loop { 64 | let secs = clock.restart().as_seconds(); 65 | 66 | while let Some(ev) = window.poll_event() { 67 | if ev == Event::Closed { 68 | window.close(); 69 | return; 70 | } 71 | } 72 | 73 | update_joypad_state(&mut gameboy); 74 | // gameboy.step_until_full_audio_buffer(); 75 | 76 | // Draw the previous frame 77 | screen_texture.update_from_pixels( 78 | &gameboy.gpu.get_rgba_frame(), 79 | sw, 80 | sh, 81 | 0, 82 | 0, 83 | ); 84 | let mut screen_sprite = Sprite::with_texture(&screen_texture); 85 | screen_sprite.set_scale(sprite_scale); 86 | 87 | window.clear(Color::BLACK); 88 | window.draw(&screen_sprite); 89 | if DRAW_FPS { 90 | text.as_mut() 91 | .unwrap() 92 | .set_string(&format!("{} FPS", (1. / secs) as usize)[..]); 93 | window.draw(text.as_ref().unwrap()); 94 | } 95 | window.display(); 96 | 97 | // Play the audio while creating the next frame and sound buffer 98 | // This way we're not idling, we're actively computing the next event. 99 | // let sound_buffer = SoundBuffer::from_samples(&gameboy.mem.apu.buffer, 2, SOUND_SAMPLE_RATE as u32).unwrap(); 100 | // let mut sound = Sound::with_buffer(&sound_buffer); 101 | // sound.play(); 102 | 103 | let mut sound_backing_store = SOUND_BACKING_STORE.lock(); 104 | *sound_backing_store = gameboy.mem.apu.buffer; 105 | let sound_buffer = SoundBuffer::from_samples( 106 | &*sound_backing_store, 107 | 2, 108 | SOUND_SAMPLE_RATE as u32, 109 | ) 110 | .unwrap(); 111 | let mut sound = Sound::with_buffer(&sound_buffer); 112 | 113 | // sound.set_volume(0.); 114 | sound.play(); 115 | while sound.status() == SoundStatus::PLAYING { 116 | if !gameboy.mem.apu.buffer_full { 117 | gameboy.step(); 118 | } else { 119 | // We're finished with this frame. Let's just wait for audio 120 | // to sync up. 121 | std::hint::spin_loop(); 122 | } 123 | } 124 | 125 | // Just in-case we're running too slow, let's catch up. 126 | // This may be when you get a small audio pop. It happens more often 127 | // on slower machines. 128 | while !gameboy.mem.apu.buffer_full { 129 | gameboy.step(); 130 | } 131 | gameboy.mem.apu.buffer_full = false; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /sfml-gui/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod control; 2 | pub mod gui; 3 | 4 | use std::env; 5 | 6 | use gbrs_core::config::Config; 7 | use gbrs_core::cpu::Cpu; 8 | use gbrs_core::memory::rom::Rom; 9 | use gui::run_gui; 10 | 11 | // TODO: Get these from an SFML audio device 12 | const SOUND_BUFFER_SIZE: usize = 1024; 13 | const SOUND_SAMPLE_RATE: usize = 48000; 14 | 15 | fn main() { 16 | let rom_path = env::args().nth(1).expect("Pass a ROM path as an argument"); 17 | let processor = Cpu::from_config(Config { 18 | sound_buffer_size: SOUND_BUFFER_SIZE, 19 | sound_sample_rate: SOUND_SAMPLE_RATE, 20 | rom: Rom::from_file(&rom_path), 21 | }); 22 | run_gui(processor); 23 | } 24 | -------------------------------------------------------------------------------- /wasm-gui/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "base64" 7 | version = "0.20.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" 10 | 11 | [[package]] 12 | name = "bumpalo" 13 | version = "3.11.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "console_error_panic_hook" 25 | version = "0.1.7" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 28 | dependencies = [ 29 | "cfg-if", 30 | "wasm-bindgen", 31 | ] 32 | 33 | [[package]] 34 | name = "gbrs-core" 35 | version = "0.2.0" 36 | dependencies = [ 37 | "lazy_static", 38 | "smallvec", 39 | ] 40 | 41 | [[package]] 42 | name = "gbrs-wasm-gui" 43 | version = "0.1.0" 44 | dependencies = [ 45 | "base64", 46 | "console_error_panic_hook", 47 | "gbrs-core", 48 | "wasm-bindgen", 49 | "web-sys", 50 | ] 51 | 52 | [[package]] 53 | name = "js-sys" 54 | version = "0.3.60" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 57 | dependencies = [ 58 | "wasm-bindgen", 59 | ] 60 | 61 | [[package]] 62 | name = "lazy_static" 63 | version = "1.4.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 66 | 67 | [[package]] 68 | name = "log" 69 | version = "0.4.17" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 72 | dependencies = [ 73 | "cfg-if", 74 | ] 75 | 76 | [[package]] 77 | name = "once_cell" 78 | version = "1.16.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 81 | 82 | [[package]] 83 | name = "proc-macro2" 84 | version = "1.0.49" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 87 | dependencies = [ 88 | "unicode-ident", 89 | ] 90 | 91 | [[package]] 92 | name = "quote" 93 | version = "1.0.23" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 96 | dependencies = [ 97 | "proc-macro2", 98 | ] 99 | 100 | [[package]] 101 | name = "smallvec" 102 | version = "1.10.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 105 | 106 | [[package]] 107 | name = "syn" 108 | version = "1.0.107" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 111 | dependencies = [ 112 | "proc-macro2", 113 | "quote", 114 | "unicode-ident", 115 | ] 116 | 117 | [[package]] 118 | name = "unicode-ident" 119 | version = "1.0.6" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 122 | 123 | [[package]] 124 | name = "wasm-bindgen" 125 | version = "0.2.83" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 128 | dependencies = [ 129 | "cfg-if", 130 | "wasm-bindgen-macro", 131 | ] 132 | 133 | [[package]] 134 | name = "wasm-bindgen-backend" 135 | version = "0.2.83" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 138 | dependencies = [ 139 | "bumpalo", 140 | "log", 141 | "once_cell", 142 | "proc-macro2", 143 | "quote", 144 | "syn", 145 | "wasm-bindgen-shared", 146 | ] 147 | 148 | [[package]] 149 | name = "wasm-bindgen-macro" 150 | version = "0.2.83" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 153 | dependencies = [ 154 | "quote", 155 | "wasm-bindgen-macro-support", 156 | ] 157 | 158 | [[package]] 159 | name = "wasm-bindgen-macro-support" 160 | version = "0.2.83" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 163 | dependencies = [ 164 | "proc-macro2", 165 | "quote", 166 | "syn", 167 | "wasm-bindgen-backend", 168 | "wasm-bindgen-shared", 169 | ] 170 | 171 | [[package]] 172 | name = "wasm-bindgen-shared" 173 | version = "0.2.83" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 176 | 177 | [[package]] 178 | name = "web-sys" 179 | version = "0.3.60" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 182 | dependencies = [ 183 | "js-sys", 184 | "wasm-bindgen", 185 | ] 186 | -------------------------------------------------------------------------------- /wasm-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gbrs-wasm-gui" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | gbrs-core = { path = "../core" } 11 | wasm-bindgen = "0.2" 12 | web-sys = { version = "0.3.77", features = ["console", "Window", "Storage"] } 13 | console_error_panic_hook = "0.1.7" 14 | base64 = "0.22.1" -------------------------------------------------------------------------------- /wasm-gui/buildAndServe.sh: -------------------------------------------------------------------------------- 1 | wasm-pack build --release --target web && \ 2 | python3 -m http.server -------------------------------------------------------------------------------- /wasm-gui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gbrs 5 | 6 | 20 | 21 | 22 | 23 | 24 | 25 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /wasm-gui/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gbrs_core::config::Config; 2 | use gbrs_core::constants; 3 | use gbrs_core::cpu::Cpu; 4 | use gbrs_core::memory::rom::Rom; 5 | use gbrs_core::{callbacks, callbacks::Callbacks, constants::*}; 6 | use wasm_bindgen::prelude::*; 7 | use web_sys::{console, window, Storage}; 8 | 9 | static mut CPU: Option = None; 10 | 11 | fn local_storage() -> Storage { 12 | window().unwrap().local_storage().unwrap().unwrap() 13 | } 14 | 15 | #[wasm_bindgen] 16 | pub fn create_gameboy() { 17 | console_error_panic_hook::set_once(); 18 | 19 | unsafe { 20 | callbacks::set_callbacks(Callbacks { 21 | log: |log_str| console::log_1(&log_str.into()), 22 | save: |game_name, _rom_path, save_data| { 23 | let data_string = base64::encode(save_data); 24 | local_storage() 25 | .set_item(game_name, &data_string) 26 | .expect("Failed to save in localStorage"); 27 | }, 28 | load: |game_name, _rom_path, expected_size| { 29 | let optional_data_string = local_storage() 30 | .get_item(game_name) 31 | .expect("Failed to read save in localStorage"); 32 | 33 | if let Some(data_string) = optional_data_string { 34 | // This game already has save data in this browser 35 | let loaded_data = base64::decode(data_string).unwrap(); 36 | if loaded_data.len() == expected_size { 37 | return loaded_data; 38 | } 39 | } 40 | // Else we've not run this game before 41 | vec![0; expected_size as usize] 42 | }, 43 | }); 44 | 45 | CPU = Some(Cpu::from_config(Config { 46 | sound_buffer_size: constants::SOUND_BUFFER_SIZE, 47 | sound_sample_rate: constants::SOUND_SAMPLE_RATE, 48 | rom: Rom::from_bytes( 49 | include_bytes!("../../roms/dmg-acid2.gb").to_vec(), 50 | ), 51 | })); 52 | } 53 | } 54 | 55 | #[wasm_bindgen] 56 | pub fn step_one_frame() { 57 | unsafe { 58 | CPU.as_mut().unwrap().step_one_frame(); 59 | } 60 | } 61 | 62 | #[wasm_bindgen] 63 | pub fn get_finished_frame() -> Vec { 64 | let frame = unsafe { CPU.as_mut().unwrap().gpu.finished_frame }; 65 | // TODO: Re-use a buffer instead 66 | let mut int_frame = Vec::with_capacity(SCREEN_BUFFER_SIZE); 67 | 68 | for i in 0..SCREEN_BUFFER_SIZE { 69 | int_frame.push(format!( 70 | "rgb({},{},{})", 71 | frame[i].red, frame[i].green, frame[i].blue 72 | )); 73 | } 74 | 75 | int_frame 76 | } 77 | 78 | #[wasm_bindgen] 79 | pub fn set_control_state( 80 | a: bool, 81 | b: bool, 82 | up: bool, 83 | down: bool, 84 | left: bool, 85 | right: bool, 86 | start: bool, 87 | select: bool, 88 | ) { 89 | unsafe { 90 | let cpu = CPU.as_mut().unwrap(); 91 | cpu.mem.joypad.a_pressed = a; 92 | cpu.mem.joypad.b_pressed = b; 93 | cpu.mem.joypad.up_pressed = up; 94 | cpu.mem.joypad.down_pressed = down; 95 | cpu.mem.joypad.left_pressed = left; 96 | cpu.mem.joypad.right_pressed = right; 97 | cpu.mem.joypad.start_pressed = start; 98 | cpu.mem.joypad.select_pressed = select; 99 | } 100 | } 101 | --------------------------------------------------------------------------------