From 4acf98bcbd4bf983d3d232300fc13ba742ce3af9 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 12:16:52 +0100 Subject: [PATCH 01/12] replace mgba-test-runner with better bindings --- .github/workflows/build-and-test.yml | 4 +- .gitmodules | 3 + README.md | 2 +- emulator/.gitignore | 1 + emulator/Cargo.lock | 731 +++++++++++++++++++++ emulator/Cargo.toml | 6 + emulator/mgba-sys/Cargo.toml | 20 + emulator/mgba-sys/build.rs | 76 +++ emulator/mgba-sys/mgba | 1 + emulator/mgba-sys/src/lib.rs | 11 + emulator/mgba-sys/wrapper.h | 6 + emulator/mgba/Cargo.toml | 11 + emulator/mgba/save.gba | Bin 0 -> 8192 bytes emulator/mgba/src/lib.rs | 216 ++++++ emulator/mgba/src/log.rs | 143 ++++ emulator/mgba/src/vfile.rs | 266 ++++++++ emulator/mgba/src/vfile/file.rs | 44 ++ emulator/mgba/src/vfile/memory.rs | 61 ++ emulator/mgba/src/vfile/shared.rs | 64 ++ emulator/mgba/test.gba | Bin 0 -> 262144 bytes emulator/test-runner/Cargo.toml | 13 + emulator/test-runner/src/image_compare.rs | 64 ++ emulator/test-runner/src/main.rs | 163 +++++ mgba-test-runner/Cargo.lock | 231 ------- mgba-test-runner/Cargo.toml | 13 - mgba-test-runner/add_cycles_register.patch | 40 -- mgba-test-runner/build-mgba.sh | 47 -- mgba-test-runner/build.rs | 34 - mgba-test-runner/c/test-runner.c | 142 ---- mgba-test-runner/c/test-runner.h | 23 - mgba-test-runner/create-bindings.sh | 3 - mgba-test-runner/src/bindings.rs | 330 ---------- mgba-test-runner/src/main.rs | 196 ------ mgba-test-runner/src/runner.rs | 111 ---- 34 files changed, 1903 insertions(+), 1173 deletions(-) create mode 100644 .gitmodules create mode 100644 emulator/.gitignore create mode 100644 emulator/Cargo.lock create mode 100644 emulator/Cargo.toml create mode 100644 emulator/mgba-sys/Cargo.toml create mode 100644 emulator/mgba-sys/build.rs create mode 160000 emulator/mgba-sys/mgba create mode 100644 emulator/mgba-sys/src/lib.rs create mode 100644 emulator/mgba-sys/wrapper.h create mode 100644 emulator/mgba/Cargo.toml create mode 100644 emulator/mgba/save.gba create mode 100644 emulator/mgba/src/lib.rs create mode 100644 emulator/mgba/src/log.rs create mode 100644 emulator/mgba/src/vfile.rs create mode 100644 emulator/mgba/src/vfile/file.rs create mode 100644 emulator/mgba/src/vfile/memory.rs create mode 100644 emulator/mgba/src/vfile/shared.rs create mode 100644 emulator/mgba/test.gba create mode 100644 emulator/test-runner/Cargo.toml create mode 100644 emulator/test-runner/src/image_compare.rs create mode 100644 emulator/test-runner/src/main.rs delete mode 100644 mgba-test-runner/Cargo.lock delete mode 100644 mgba-test-runner/Cargo.toml delete mode 100644 mgba-test-runner/add_cycles_register.patch delete mode 100644 mgba-test-runner/build-mgba.sh delete mode 100644 mgba-test-runner/build.rs delete mode 100644 mgba-test-runner/c/test-runner.c delete mode 100644 mgba-test-runner/c/test-runner.h delete mode 100755 mgba-test-runner/create-bindings.sh delete mode 100644 mgba-test-runner/src/bindings.rs delete mode 100644 mgba-test-runner/src/main.rs delete mode 100644 mgba-test-runner/src/runner.rs diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d2c62720..2f05c394 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -31,11 +31,11 @@ jobs: ~/.cargo/registry ~/.cargo/git ~/target - mgba-test-runner/target + emulator/target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: install mgba-test-runner - run: cargo install --path mgba-test-runner --verbose + run: cargo install --path emulator/test-runner --verbose - name: Set CARGO_TARGET_DIR run: echo "CARGO_TARGET_DIR=$HOME/target" >> $GITHUB_ENV - uses: extractions/setup-just@v1 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..3bf4ef24 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "emulator/mgba-sys/mgba"] + path = emulator/mgba-sys/mgba + url = https://github.com/mgba-emu/mgba.git diff --git a/README.md b/README.md index feed710f..e5bead75 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ to just write games for the Game Boy Advance using this library: - Debian and derivatives: `sudo apt install libelf-dev cmake` - Arch Linux and derivatives: `pacman -S libelf cmake` - mgba-test-runner - - Run `cargo install --path mgba-test-runner` inside this directory + - Run `cargo install --path emulator/test-runner` inside this directory - [The 'just' build tool](https://github.com/casey/just) - Install with `cargo install just` - [mdbook](https://rust-lang.github.io/mdBook/index.html) diff --git a/emulator/.gitignore b/emulator/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/emulator/.gitignore @@ -0,0 +1 @@ +/target diff --git a/emulator/Cargo.lock b/emulator/Cargo.lock new file mode 100644 index 00000000..a405fd5e --- /dev/null +++ b/emulator/Cargo.lock @@ -0,0 +1,731 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "agb-gbafix" +version = "0.16.0" +dependencies = [ + "anyhow", + "clap", + "elf", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags 2.3.3", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "elf" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b183d6ce6ca4cf30e3db37abf5b52568b5f9015c97d9fbdd7026aa5dcdd758" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mgba" +version = "0.1.0" +dependencies = [ + "libc", + "mgba-sys", + "thiserror", +] + +[[package]] +name = "mgba-sys" +version = "0.1.0" +dependencies = [ + "bindgen", + "cmake", + "pkg-config", +] + +[[package]] +name = "mgba-test-runner" +version = "0.1.0" +dependencies = [ + "agb-gbafix", + "anyhow", + "clap", + "image", + "mgba", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "prettyplease" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/emulator/Cargo.toml b/emulator/Cargo.toml new file mode 100644 index 00000000..ac17e7bb --- /dev/null +++ b/emulator/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = [ + "mgba", + "mgba-sys", + "test-runner" +] \ No newline at end of file diff --git a/emulator/mgba-sys/Cargo.toml b/emulator/mgba-sys/Cargo.toml new file mode 100644 index 00000000..1661fae5 --- /dev/null +++ b/emulator/mgba-sys/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mgba-sys" +version = "0.1.0" +edition = "2021" +exclude = [ + "mgba/doc/*", + "mgba/res/*", + "mgba/cinema/*", + "mgba/tools/*", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + + +[build-dependencies] +bindgen = "0.66" +pkg-config = "0.3.27" +cmake = "0.1" \ No newline at end of file diff --git a/emulator/mgba-sys/build.rs b/emulator/mgba-sys/build.rs new file mode 100644 index 00000000..35a543e9 --- /dev/null +++ b/emulator/mgba-sys/build.rs @@ -0,0 +1,76 @@ +use std::{ + env, + error::Error, + path::{Path, PathBuf}, + process::Command, +}; + +type MyError = Result<(), Box>; + +fn main() -> MyError { + have_submodule()?; + + generate_bindings()?; + + compile()?; + + Ok(()) +} + +fn have_submodule() -> MyError { + if !Path::new("mgba/src").exists() { + let _ = Command::new("git") + .args(["submodule", "update", "--init", "mgba"]) + .status()?; + } + + Ok(()) +} + +fn generate_bindings() -> MyError { + println!("cargo:rerun-if-changed=wrapper.h"); + + let bindings = bindgen::Builder::default() + .header("wrapper.h") + .opaque_type("mTiming") + .allowlist_type("mCore") + .allowlist_type("VFile") + .allowlist_type("VDir") + .allowlist_type("mLogger") + .allowlist_type("mLogLevel") + .allowlist_var("MAP_WRITE") + .allowlist_var("BYTES_PER_PIXEL") + .allowlist_function("GBACoreCreate") + .allowlist_function("mCoreInitConfig") + .allowlist_function("mLogSetDefaultLogger") + .allowlist_function("blip_set_rates") + .allowlist_function("blip_read_samples") + .allowlist_function("blip_samples_avail") + .allowlist_function("mCoreConfigLoadDefaults") + .allowlist_function("mCoreLoadConfig") + .allowlist_function("mTimingGlobalTime") + .allowlist_function("mLogCategoryName") + .generate_cstr(true) + .derive_default(true) + .generate()?; + + let out_path = PathBuf::from(env::var("OUT_DIR")?); + bindings.write_to_file(out_path.join("bindings.rs"))?; + + Ok(()) +} + +fn compile() -> MyError { + let dst = cmake::Config::new("mgba") + .define("LIBMGBA_ONLY", "1") + .define("M_CORE_GBA", "1") + .define("M_CORE_GB", "0") + .define("USE_DEBUGGERS", "1") + .build(); + + println!("cargo:rustc-link-search=native={}/lib", dst.display()); + println!("cargo:rustc-link-search=native={}", dst.display()); + println!("cargo:rustc-link-lib=static=mgba"); + + Ok(()) +} diff --git a/emulator/mgba-sys/mgba b/emulator/mgba-sys/mgba new file mode 160000 index 00000000..2fb55450 --- /dev/null +++ b/emulator/mgba-sys/mgba @@ -0,0 +1 @@ +Subproject commit 2fb55450610a4d10479a1eed1408f905d79318e3 diff --git a/emulator/mgba-sys/src/lib.rs b/emulator/mgba-sys/src/lib.rs new file mode 100644 index 00000000..cefe05ce --- /dev/null +++ b/emulator/mgba-sys/src/lib.rs @@ -0,0 +1,11 @@ +#[allow(non_upper_case_globals)] +#[allow(non_camel_case_types)] +#[allow(non_snake_case)] +mod ffi { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +pub use ffi::*; + +unsafe impl Sync for mLogger {} +unsafe impl Send for mLogger {} diff --git a/emulator/mgba-sys/wrapper.h b/emulator/mgba-sys/wrapper.h new file mode 100644 index 00000000..d11189fb --- /dev/null +++ b/emulator/mgba-sys/wrapper.h @@ -0,0 +1,6 @@ +#include "mgba/include/mgba-util/vfs.h" +#include "mgba/include/mgba/core/blip_buf.h" +#include "mgba/include/mgba/core/core.h" +#include "mgba/include/mgba/core/log.h" +#include "mgba/include/mgba/core/timing.h" +#include "mgba/include/mgba/gba/core.h" \ No newline at end of file diff --git a/emulator/mgba/Cargo.toml b/emulator/mgba/Cargo.toml new file mode 100644 index 00000000..d5b152f0 --- /dev/null +++ b/emulator/mgba/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mgba" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libc = "0.2.147" +mgba-sys = { version = "0.1.0", path = "../mgba-sys" } +thiserror = "1" \ No newline at end of file diff --git a/emulator/mgba/save.gba b/emulator/mgba/save.gba new file mode 100644 index 0000000000000000000000000000000000000000..84c9fe4acc4c5e90fde711248f3269be8777b4fa GIT binary patch literal 8192 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 literal 0 HcmV?d00001 diff --git a/emulator/mgba/src/lib.rs b/emulator/mgba/src/lib.rs new file mode 100644 index 00000000..9f9d8777 --- /dev/null +++ b/emulator/mgba/src/lib.rs @@ -0,0 +1,216 @@ +mod log; +mod vfile; + +use std::ptr::NonNull; + +pub use log::{Logger, LogLevel}; +pub use vfile::{file::FileBacked, memory::MemoryBacked, shared::Shared, MapFlag, VFile}; + +use vfile::VFileAlloc; + +pub struct MCore { + core: NonNull, + video_buffer: Box<[u32]>, +} + +impl Drop for MCore { + fn drop(&mut self) { + unsafe { self.core.as_ref().deinit.unwrap()(self.core.as_ptr()) } + } +} + +const SAMPLE_RATE: f64 = 44100.0; + +macro_rules! call_on_core { + ($core:expr => $fn_name:ident($($arg:expr),* $(,)?)) => { + $core.as_ref().$fn_name.unwrap()($core.as_ptr(), $($arg),*) + }; +} + +pub fn set_global_default_logger(logger: &'static Logger) { + unsafe { mgba_sys::mLogSetDefaultLogger(logger.to_mgba()) } +} + +impl MCore { + pub fn new() -> Option { + set_global_default_logger(&log::NO_LOGGER); + + let core = unsafe { mgba_sys::GBACoreCreate() }; + let core = NonNull::new(core)?; + + unsafe { mgba_sys::mCoreInitConfig(core.as_ptr(), std::ptr::null()) }; + + unsafe { call_on_core!(core=>init()) }; + + let (mut width, mut height) = (0, 0); + + unsafe { call_on_core!(core=>desiredVideoDimensions(&mut width, &mut height)) }; + + let mut video_buffer = vec![ + 0; + (width * height * mgba_sys::BYTES_PER_PIXEL) as usize + / std::mem::size_of::() + ] + .into_boxed_slice(); + + unsafe { + call_on_core!( + core=>setVideoBuffer( + video_buffer.as_mut_ptr(), + width as usize + ) + ) + } + + unsafe { call_on_core!(core=>reset()) }; + + unsafe { call_on_core!(core=>setAudioBufferSize(0x4000)) }; + + unsafe { + mgba_sys::blip_set_rates( + call_on_core!(core=>getAudioChannel(0)), + call_on_core!(core=>frequency()) as f64, + SAMPLE_RATE, + ) + } + unsafe { + mgba_sys::blip_set_rates( + call_on_core!(core=>getAudioChannel(1)), + call_on_core!(core=>frequency()) as f64, + SAMPLE_RATE, + ) + } + + let core_options = mgba_sys::mCoreOptions { + volume: 0x100, + useBios: true, + ..Default::default() + }; + + unsafe { mgba_sys::mCoreConfigLoadDefaults(&mut (*core.as_ptr()).config, &core_options) }; + unsafe { mgba_sys::mCoreLoadConfig(core.as_ptr()) }; + + Some(MCore { core, video_buffer }) + } + + pub fn load_rom(&mut self, vfile: V) { + let vfile = VFileAlloc::new(vfile); + unsafe { call_on_core!(self.core=>loadROM(vfile.into_mgba())) }; + } + + pub fn frame(&mut self) { + unsafe { call_on_core!(self.core=>runFrame()) }; + } + + pub fn step(&mut self) { + unsafe { call_on_core!(self.core=>step()) }; + } + + pub fn set_keys(&mut self, buttons: u32) { + unsafe { call_on_core!(self.core=>setKeys(buttons)) }; + } + + pub fn load_save(&mut self, save_file: V) { + let save_file = VFileAlloc::new(save_file); + unsafe { + call_on_core!(self.core=>loadSave(save_file.into_mgba())); + } + } + + pub fn video_buffer(&mut self) -> &[u32] { + self.video_buffer.as_ref() + } + + pub fn current_cycle(&mut self) -> u64 { + unsafe { mgba_sys::mTimingGlobalTime(self.core.as_ref().timing) } + } + + pub fn read_audio(&mut self, target: &mut [i16]) -> usize { + let audio_channel_left = unsafe { call_on_core!(self.core=>getAudioChannel(0)) }; + let audio_channel_right = unsafe { call_on_core!(self.core=>getAudioChannel(1)) }; + + let samples_available = unsafe { mgba_sys::blip_samples_avail(audio_channel_left) }; + + if samples_available > 0 { + let samples_to_read = samples_available.min(target.len() as i32 / 2); + let produced = unsafe { + mgba_sys::blip_read_samples( + audio_channel_left, + target.as_mut_ptr().cast(), + samples_to_read, + 1, + ) + }; + unsafe { + mgba_sys::blip_read_samples( + audio_channel_right, + target.as_mut_ptr().add(1).cast(), + samples_to_read, + 1, + ) + }; + + return produced.try_into().unwrap(); + } + + 0 + } +} + +#[cfg(test)] +mod tests { + + static TEST_ROM: &[u8] = include_bytes!("../test.gba"); + static SAVE_TEST_ROM: &[u8] = include_bytes!("../save.gba"); + + use super::*; + + #[test] + fn check_running_game_for_some_frames() { + let file = MemoryBacked::new_from_slice(TEST_ROM); + let mut core = MCore::new().unwrap(); + core.load_rom(file); + + for _ in 0..100 { + core.frame(); + } + } + + #[test] + fn check_save_file_is_initialised() { + let shared_save_file = Shared::new(MemoryBacked::new(Vec::new())); + + { + let save_file = shared_save_file.clone(); + let rom_file = MemoryBacked::new_from_slice(SAVE_TEST_ROM); + + let mut core = MCore::new().unwrap(); + core.load_rom(rom_file); + core.load_save(save_file); + for _ in 0..10 { + core.frame(); + } + } + + let save_file = shared_save_file + .try_into_inner() + .unwrap_or_else(|_| panic!("the shared references were not released")) + .into_inner() + .into_owned(); + + assert_eq!(save_file.len(), 32 * 1024, "the save file should be 32 kb"); + + assert_eq!( + save_file[0..128], + (0..128).collect::>(), + "First 128 bytes should be ascending numbers" + ); + assert_eq!( + save_file[128..], + std::iter::repeat(0xFF) + .take(save_file.len() - 128) + .collect::>(), + "Remanider of save should be 0xFF, all ones" + ); + } +} diff --git a/emulator/mgba/src/log.rs b/emulator/mgba/src/log.rs new file mode 100644 index 00000000..663a2421 --- /dev/null +++ b/emulator/mgba/src/log.rs @@ -0,0 +1,143 @@ +use std::ffi::CStr; + +use thiserror::Error; + +pub static NO_LOGGER: Logger = generate_no_logger(); + +pub enum LogLevel { + Fatal, + Error, + Warn, + Info, + Debug, + Stub, + GameError, + Unknown, +} + + +#[derive(Debug, Error)] +#[error("A log level of {provided_log_level} does not match any known log level")] +pub struct LogLevelIsNotValid { + provided_log_level: mgba_sys::mLogLevel, +} + +impl TryFrom for LogLevel { + type Error = LogLevelIsNotValid; + + fn try_from(value: mgba_sys::mLogLevel) -> Result { + Ok(match value { + mgba_sys::mLogLevel_mLOG_FATAL => LogLevel::Fatal, + mgba_sys::mLogLevel_mLOG_ERROR => LogLevel::Error, + mgba_sys::mLogLevel_mLOG_WARN => LogLevel::Warn, + mgba_sys::mLogLevel_mLOG_INFO => LogLevel::Info, + mgba_sys::mLogLevel_mLOG_DEBUG => LogLevel::Debug, + mgba_sys::mLogLevel_mLOG_STUB => LogLevel::Stub, + mgba_sys::mLogLevel_mLOG_GAME_ERROR => LogLevel::GameError, + _ => return Err(LogLevelIsNotValid { + provided_log_level: value + }) + }) + } +} + +const fn generate_no_logger() -> Logger { + Logger { + logger: mgba_sys::mLogger { + log: Some(no_log), + filter: std::ptr::null_mut(), + }, + log: None, + } +} + +#[repr(C)] +pub struct Logger { + logger: mgba_sys::mLogger, + log: Option, +} + +impl Logger { + pub(crate) fn to_mgba(&'static self) -> *mut mgba_sys::mLogger { + (self as *const Logger) + .cast::() + .cast_mut() + } + + pub const fn new(logger: fn(&str, LogLevel, String)) -> Self { + Logger { + logger: mgba_sys::mLogger { + log: Some(log_string_wrapper), + filter: std::ptr::null_mut(), + }, + log: Some(logger), + } + } +} + +extern "C" fn log_string_wrapper( + logger: *mut mgba_sys::mLogger, + category: i32, + level: u32, + format: *const i8, + args: VaArgs, +) { + let logger = logger.cast::(); + if let Some(logger) = unsafe { &(*logger).log } { + let s = convert_to_string(format, args); + + if let Some(s) = s { + let category_c_name = unsafe { mgba_sys::mLogCategoryName(category) }; + const UNKNOWN: &str = "Unknown"; + let category_name = if category_c_name.is_null() { + UNKNOWN + } else { + unsafe { CStr::from_ptr(category_c_name).to_str() }.unwrap_or(UNKNOWN) + }; + + logger(category_name, LogLevel::try_from(level).unwrap_or(LogLevel::Unknown), s); + } + } +} + +#[cfg(unix)] +type VaArgs = *mut mgba_sys::__va_list_tag; + +#[cfg(windows)] +type VaArgs = mgba_sys::va_list; + +extern "C" { + fn vsnprintf( + s: *mut libc::c_char, + n: libc::size_t, + format: *const libc::c_char, + va_args: VaArgs, + ) -> std::ffi::c_int; +} + +fn convert_to_string(format: *const i8, var_args: VaArgs) -> Option { + const BUFFER_SIZE: usize = 1024; + + let mut string = vec![0u8; BUFFER_SIZE]; + + let count = unsafe { vsnprintf(string.as_mut_ptr().cast(), BUFFER_SIZE, format, var_args) }; + if count < 0 { + return None; + } + // The last byte is always null, so guarentee we can remove that. If we + // wrote too much in this sprint then we can at least partially recover the + // string. + string.truncate(string.len() - 1); + string.truncate(count as usize); + + String::from_utf8(string).ok() +} + +extern "C" fn no_log( + _l: *mut mgba_sys::mLogger, + _category: i32, + _level: u32, + _format: *const i8, + _var_args: VaArgs, +) { +} diff --git a/emulator/mgba/src/vfile.rs b/emulator/mgba/src/vfile.rs new file mode 100644 index 00000000..c6ac8e95 --- /dev/null +++ b/emulator/mgba/src/vfile.rs @@ -0,0 +1,266 @@ +use std::io::Read; +use std::io::Result; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Write; + +pub mod file; +pub mod memory; +pub mod shared; + +pub enum MapFlag { + Read, + Write, +} + +pub trait VFile: Seek + Read + Write { + fn readline(&mut self, buffer: &mut [u8]) -> Result { + let mut byte = 0; + while byte < buffer.len() - 1 { + if self.read(&mut buffer[byte..byte + 1])? == 0 { + break; + } + + byte += 1; + if buffer[byte - 1] == b'\n' { + break; + } + } + buffer[byte] = b'\0'; + Ok(byte) + } + fn map(&mut self, size: usize, _flag: MapFlag) -> Result> { + let position = self.stream_position()?; + let data = vec![0; size]; + let mut data = data.into_boxed_slice(); + + self.seek(SeekFrom::Start(0))?; + match self.read_exact(&mut data) { + Ok(_) => {} + Err(err) => match err.kind() { + std::io::ErrorKind::UnexpectedEof => {} + _ => return Err(err), + }, + } + self.seek(SeekFrom::Start(position))?; + + Ok(data) + } + fn unmap(&mut self, data: Box<[u8]>) -> Result<()> { + // assume map was created with write + let position = self.stream_position()?; + self.seek(SeekFrom::Start(0))?; + self.write_all(&data)?; + self.seek(SeekFrom::Start(position))?; + + Ok(()) + } + fn truncate(&mut self, size: usize) -> Result<()> { + let position = self.stream_position()?; + let stream_length = self.seek(SeekFrom::End(0))?; + + if (size as u64) > stream_length { + self.seek(SeekFrom::Start(position))?; + return Ok(()); + } + + self.seek(SeekFrom::Start(size as u64))?; + + let bytes_to_write = stream_length - size as u64; + let bytes: Vec = std::iter::repeat(0).take(bytes_to_write as usize).collect(); + self.write_all(&bytes)?; + + self.seek(SeekFrom::Start(position))?; + + Ok(()) + } + fn size(&mut self) -> Result { + let position = self.stream_position()?; + let stream_length = self.seek(SeekFrom::End(0))?; + self.seek(SeekFrom::Start(position))?; + + Ok(stream_length as usize) + } + fn sync(&mut self, buffer: &[u8]) -> Result<()> { + let position = self.stream_position()?; + + self.seek(SeekFrom::Start(0))?; + self.write_all(buffer)?; + self.seek(SeekFrom::Start(position))?; + + Ok(()) + } +} + +#[repr(C)] +struct VFileInner { + vfile: mgba_sys::VFile, + file: V, +} + +pub struct VFileAlloc(Box>); + +impl VFileAlloc { + pub fn new(f: V) -> Self { + Self(Box::new(VFileInner { + vfile: unsafe { vfile_extern::create_vfile::() }, + file: f, + })) + } + + pub(crate) fn into_mgba(self) -> *mut mgba_sys::VFile { + let f = Box::into_raw(self.0) as *mut VFileInner; + f.cast() + } +} + +mod vfile_extern { + use std::io::SeekFrom; + + /// Safety: Must be part of a VFileInner + pub unsafe fn create_vfile() -> mgba_sys::VFile { + mgba_sys::VFile { + close: Some(close::), + seek: Some(seek::), + read: Some(read::), + readline: Some(readline::), + write: Some(write::), + map: Some(map::), + unmap: Some(unmap::), + truncate: Some(truncate::), + size: Some(size::), + sync: Some(sync::), + } + } + + use mgba_sys::VFile; + + extern "C" fn close(vf: *mut VFile) -> bool { + drop(unsafe { Box::from_raw(vf.cast::>()) }); + true + } + + unsafe fn with_inner(vf: *mut VFile, f: F) -> T + where + F: FnOnce(&mut dyn super::VFile) -> T, + { + let vf = vf.cast::>(); + let vf = &mut *vf; + f(&mut vf.file) + } + + extern "C" fn seek( + vf: *mut VFile, + offset: mgba_sys::off_t, + whence: std::os::raw::c_int, + ) -> mgba_sys::off_t { + unsafe { + with_inner::(vf, |vf| { + // casts required for windows compatability + #[allow(clippy::useless_conversion)] + let seek = match whence { + libc::SEEK_CUR => SeekFrom::Current(offset.into()), + libc::SEEK_SET => SeekFrom::Start(offset as u64), + libc::SEEK_END => SeekFrom::End(offset.into()), + _ => return -1, + }; + vf.seek(seek).map(|x| x as mgba_sys::off_t).unwrap_or(-1) + }) + } + } + + extern "C" fn read( + vf: *mut VFile, + buffer: *mut ::std::os::raw::c_void, + size: usize, + ) -> isize { + unsafe { + with_inner::(vf, |vf| { + vf.read(std::slice::from_raw_parts_mut(buffer.cast(), size)) + .map(|x| x as isize) + .unwrap_or(-1) + }) + } + } + + extern "C" fn readline( + vf: *mut VFile, + buffer: *mut ::std::os::raw::c_char, + size: usize, + ) -> isize { + unsafe { + with_inner::(vf, |vf| { + vf.readline(std::slice::from_raw_parts_mut(buffer.cast(), size)) + .map(|x| x as isize) + .unwrap_or(-1) + }) + } + } + + extern "C" fn write( + vf: *mut VFile, + buffer: *const ::std::os::raw::c_void, + size: usize, + ) -> isize { + unsafe { + with_inner::(vf, |vf| { + vf.write(std::slice::from_raw_parts(buffer.cast(), size)) + .map(|x| x as isize) + .unwrap_or(-1) + }) + } + } + + extern "C" fn map( + vf: *mut VFile, + size: usize, + flags: ::std::os::raw::c_int, + ) -> *mut ::std::os::raw::c_void { + unsafe { + with_inner::(vf, |vf| { + let map_type = match flags as u32 { + mgba_sys::MAP_WRITE => super::MapFlag::Write, + _ => super::MapFlag::Read, + }; + vf.map(size, map_type) + .map(|x| Box::leak(x).as_mut_ptr().cast()) + .unwrap_or(std::ptr::null_mut()) + }) + } + } + + extern "C" fn unmap( + vf: *mut VFile, + memory: *mut ::std::os::raw::c_void, + size: usize, + ) { + unsafe { + with_inner::(vf, |vf| { + let b = Box::from_raw(std::slice::from_raw_parts_mut(memory.cast::(), size)); + let _ = vf.unmap(b); + }) + } + } + + extern "C" fn truncate(vf: *mut VFile, size: usize) { + unsafe { + let _ = with_inner::(vf, |vf| vf.truncate(size)); + } + } + extern "C" fn size(vf: *mut VFile) -> isize { + unsafe { with_inner::(vf, |vf| vf.size().map(|x| x as isize).unwrap_or(-1)) } + } + + extern "C" fn sync( + vf: *mut VFile, + buffer: *mut ::std::os::raw::c_void, + size: usize, + ) -> bool { + unsafe { + with_inner::(vf, |vf| { + vf.sync(std::slice::from_raw_parts(buffer.cast(), size)) + .is_ok() + }) + } + } +} diff --git a/emulator/mgba/src/vfile/file.rs b/emulator/mgba/src/vfile/file.rs new file mode 100644 index 00000000..74244f9b --- /dev/null +++ b/emulator/mgba/src/vfile/file.rs @@ -0,0 +1,44 @@ +use std::{ + fs::File, + io::{Read, Seek, Write}, +}; + +use super::VFile; + +pub struct FileBacked { + file: File, +} + +impl FileBacked { + pub fn new(file: File) -> Self { + Self { file } + } + + pub fn into_inner(self) -> File { + self.file + } +} + +impl Seek for FileBacked { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.file.seek(pos) + } +} + +impl Write for FileBacked { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.file.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.file.flush() + } +} + +impl Read for FileBacked { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.file.read(buf) + } +} + +impl VFile for FileBacked {} diff --git a/emulator/mgba/src/vfile/memory.rs b/emulator/mgba/src/vfile/memory.rs new file mode 100644 index 00000000..f3082ab3 --- /dev/null +++ b/emulator/mgba/src/vfile/memory.rs @@ -0,0 +1,61 @@ +use std::{ + borrow::Cow, + io::{Cursor, Read, Seek, Write}, +}; + +use super::VFile; + +pub struct MemoryBacked { + buffer: Cursor>, +} + +impl MemoryBacked { + pub fn new_from_slice(data: &'static [u8]) -> Self { + Self { + buffer: Cursor::new(Cow::Borrowed(data)), + } + } + + pub fn new(data: Vec) -> Self { + Self { + buffer: Cursor::new(Cow::Owned(data)), + } + } + + pub fn into_inner(self) -> Cow<'static, [u8]> { + self.buffer.into_inner() + } +} + +impl Write for MemoryBacked { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let position = self.buffer.position(); + let underlying = self.buffer.get_mut().to_mut(); + let mut new_buffer = Cursor::new(underlying); + new_buffer.set_position(position); + let result = new_buffer.write(buf); + let new_position = new_buffer.position(); + + self.buffer.set_position(new_position); + + result + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Read for MemoryBacked { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.buffer.read(buf) + } +} + +impl Seek for MemoryBacked { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.buffer.seek(pos) + } +} + +impl VFile for MemoryBacked {} diff --git a/emulator/mgba/src/vfile/shared.rs b/emulator/mgba/src/vfile/shared.rs new file mode 100644 index 00000000..82d40fc3 --- /dev/null +++ b/emulator/mgba/src/vfile/shared.rs @@ -0,0 +1,64 @@ +use std::{ + io::{Read, Seek, Write}, + sync::{Arc, Mutex}, +}; + +use super::VFile; + +pub struct Shared { + inner: Arc>, +} + +impl Clone for Shared { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Shared { + pub fn new(v: V) -> Self { + Self { + inner: Arc::new(Mutex::new(v)), + } + } + + pub fn try_into_inner(self) -> Result { + Arc::try_unwrap(self.inner) + .map(|x| x.into_inner().unwrap()) + .map_err(|e| Self { inner: e }) + } +} + +impl Shared { + pub fn into_inner(self) -> V { + Arc::try_unwrap(self.inner) + .map(|x| x.into_inner().unwrap()) + .unwrap_or_else(|x| x.lock().unwrap().clone()) + } +} + +impl Write for Shared { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.inner.lock().unwrap().write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.lock().unwrap().flush() + } +} + +impl Read for Shared { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.lock().unwrap().read(buf) + } +} + +impl Seek for Shared { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.inner.lock().unwrap().seek(pos) + } +} + +impl VFile for Shared {} diff --git a/emulator/mgba/test.gba b/emulator/mgba/test.gba new file mode 100644 index 0000000000000000000000000000000000000000..29b7211aa40cfa95f5b7987d863a9dc761fca992 GIT binary patch literal 262144 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

O+laW0e>`^VTkwa=^E%{7nSW`^ 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 bool { + self.matches + } + + pub fn image(&self) -> &DynamicImage { + &self.image + } +} + +const WIDTH: usize = 240; +const HEIGHT: usize = 160; + +fn convert_rgba_to_nearest_gba_colour(c: [u8; 4]) -> [u8; 4] { + let mut n = c; + n.iter_mut() + .for_each(|a| *a = ((((*a as u32 >> 3) << 3) * 0x21) >> 5) as u8); + n +} + +pub fn compare_image( + image: impl AsRef, + video_buffer: &[u32], +) -> anyhow::Result { + let expected = Reader::open(image)?.decode()?; + let expected_buffer = expected.to_rgba8(); + + let (exp_dim_x, exp_dim_y) = expected_buffer.dimensions(); + if exp_dim_x != WIDTH as u32 || exp_dim_y != HEIGHT as u32 { + return Ok(ComparisonResult { + matches: false, + image: expected, + }); + } + + for y in 0..HEIGHT { + for x in 0..WIDTH { + let video_pixel = video_buffer[x + y * WIDTH]; + let image_pixel = expected_buffer.get_pixel(x as u32, y as u32); + let image_pixel = convert_rgba_to_nearest_gba_colour(image_pixel.0); + + if image_pixel[0..3] != video_pixel.to_le_bytes()[0..3] { + return Ok(ComparisonResult { + matches: false, + image: expected, + }); + } + } + } + + Ok(ComparisonResult { + matches: true, + image: expected, + }) +} diff --git a/emulator/test-runner/src/main.rs b/emulator/test-runner/src/main.rs new file mode 100644 index 00000000..55db72f4 --- /dev/null +++ b/emulator/test-runner/src/main.rs @@ -0,0 +1,163 @@ +use std::{ + collections::VecDeque, + error::Error, + fs::File, + io::Read, + path::{Path, PathBuf}, + sync::Mutex, +}; + +use anyhow::{anyhow, Context}; +use clap::Parser; +use image_compare::compare_image; +use mgba::{LogLevel, Logger, MCore, MemoryBacked, VFile}; + +mod image_compare; + +static LOGGER: Logger = Logger::new(my_logger); + +static LOGGER_BUFFER: Mutex> = Mutex::new(VecDeque::new()); + +fn my_logger(category: &str, level: LogLevel, s: String) { + LOGGER_BUFFER + .lock() + .unwrap() + .push_back((category.to_string(), level, s)); +} + +#[derive(Parser)] +struct CliArguments { + rom: PathBuf, +} + +struct TestRunner { + mgba: MCore, +} + +enum Timer { + Start(u64), + Total(u64), +} + +impl TestRunner { + fn new(rom: V) -> Result> { + let mut mgba = MCore::new().ok_or(anyhow!("cannot create core"))?; + + mgba::set_global_default_logger(&LOGGER); + + mgba.load_rom(rom); + + Ok(Self { mgba }) + } + + fn run(mut self) -> Result<(), Box> { + let mut timer: Timer = Timer::Total(0); + + let mut mark_tests_as_soft_failed = false; + let mut mark_this_test_as_soft_failed = false; + loop { + self.mgba.step(); + while let Some((category, level, message)) = LOGGER_BUFFER.lock().unwrap().pop_front() { + match (category.as_ref(), level, message.as_ref()) { + (_, LogLevel::Fatal, fatal_message) => { + return Err(anyhow!("Failed with fatal message: {}", fatal_message).into()); + } + ("GBA I/O", _, "Stub I/O register write: FFF800") => match timer { + Timer::Start(time) => { + let total_cycles = self.mgba.current_cycle() - time; + timer = Timer::Total(total_cycles); + } + Timer::Total(_) => { + timer = Timer::Start(self.mgba.current_cycle()); + } + }, + ("GBA Debug", _, debug_message) => { + if let Some(image_path) = debug_message.strip_prefix("image:") { + match compare_image(image_path, self.mgba.video_buffer()).with_context( + || anyhow!("Could not open image {} for comparison", image_path), + ) { + Ok(compare) => { + if !compare.success() { + eprintln!("Image and video buffer do not match"); + mark_tests_as_soft_failed = true; + mark_this_test_as_soft_failed = true; + } + } + Err(e) => eprintln!("{}", e), + } + } else if debug_message.ends_with("...") { + eprint!("{}", debug_message); + } else if debug_message == "[ok]" { + let cycles = match timer { + Timer::Start(_) => panic!("test completed with invalid timing"), + Timer::Total(c) => c, + }; + if mark_this_test_as_soft_failed { + mark_this_test_as_soft_failed = false; + eprintln!( + "[fail: {} c ≈ {} s]", + cycles, + ((cycles as f64 / (16.78 * 1_000_000.0)) * 100.0).round() + / 100.0 + ); + } else { + eprintln!( + "[ok: {} c ≈ {} s]", + cycles, + ((cycles as f64 / (16.78 * 1_000_000.0)) * 100.0).round() + / 100.0 + ); + } + } else { + eprintln!("{}", debug_message); + } + } + _ => {} + } + + if message == "Tests finished successfully" { + if mark_tests_as_soft_failed { + eprintln!("Tests failed"); + return Err(anyhow!("Tests failed").into()); + } else { + eprintln!("{}", message); + return Ok(()); + } + } + } + } + } +} + +fn main() -> Result<(), Box> { + let args = CliArguments::parse(); + + let rom = load_rom(args.rom)?; + let rom = MemoryBacked::new(rom); + + TestRunner::new(rom)?.run()?; + + Ok(()) +} + +fn load_rom>(path: P) -> anyhow::Result> { + let mut input_file = File::open(path)?; + let mut input_file_buffer = Vec::new(); + + input_file.read_to_end(&mut input_file_buffer)?; + + let mut elf_buffer = Vec::new(); + + if agb_gbafix::write_gba_file( + &input_file_buffer, + Default::default(), + agb_gbafix::PaddingBehaviour::DoNotPad, + &mut elf_buffer, + ) + .is_ok() + { + Ok(elf_buffer) + } else { + Ok(input_file_buffer) + } +} diff --git a/mgba-test-runner/Cargo.lock b/mgba-test-runner/Cargo.lock deleted file mode 100644 index 8e146309..00000000 --- a/mgba-test-runner/Cargo.lock +++ /dev/null @@ -1,231 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bytemuck" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "cc" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" -dependencies = [ - "jobserver", - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "fdeflate" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "flate2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "image" -version = "0.24.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "num-rational", - "num-traits", - "png", -] - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mgba-test-runner" -version = "0.1.0" -dependencies = [ - "anyhow", - "cc", - "image", - "regex", -] - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", - "simd-adler32", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" -dependencies = [ - "autocfg", -] - -[[package]] -name = "png" -version = "0.17.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" -dependencies = [ - "bitflags", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "regex" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" diff --git a/mgba-test-runner/Cargo.toml b/mgba-test-runner/Cargo.toml deleted file mode 100644 index 4649d2f3..00000000 --- a/mgba-test-runner/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "mgba-test-runner" -version = "0.1.0" -authors = ["Corwin Kuiper "] -edition = "2021" - -[dependencies] -regex = "1" -anyhow = "1" -image = { version = "0.24", default-features = false, features = [ "png", "bmp" ] } - -[build-dependencies] -cc = { version = "1", features = ["parallel"] } diff --git a/mgba-test-runner/add_cycles_register.patch b/mgba-test-runner/add_cycles_register.patch deleted file mode 100644 index b16d6dde..00000000 --- a/mgba-test-runner/add_cycles_register.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/include/mgba/internal/gba/io.h b/include/mgba/internal/gba/io.h -index 9875061f3..bdeafdcd3 100644 ---- a/include/mgba/internal/gba/io.h -+++ b/include/mgba/internal/gba/io.h -@@ -157,6 +157,7 @@ enum GBAIORegisters { - REG_DEBUG_STRING = 0xFFF600, - REG_DEBUG_FLAGS = 0xFFF700, - REG_DEBUG_ENABLE = 0xFFF780, -+ REG_DEBUG_CYCLES = 0xFFF800, - }; - - mLOG_DECLARE_CATEGORY(GBA_IO); -diff --git a/src/gba/io.c b/src/gba/io.c -index cc39e1192..d34dcb4b4 100644 ---- a/src/gba/io.c -+++ b/src/gba/io.c -@@ -573,6 +573,11 @@ void GBAIOWrite(struct GBA* gba, uint32_t address, uint16_t value) { - case REG_DEBUG_ENABLE: - gba->debug = value == 0xC0DE; - return; -+ case REG_DEBUG_CYCLES: { -+ int32_t number_of_cycles = mTimingCurrentTime(&gba->timing); -+ mLOG(GBA_DEBUG, INFO, "Cycles: %d Tag: %d", number_of_cycles, value); -+ return; -+ } - case REG_DEBUG_FLAGS: - if (gba->debug) { - GBADebug(gba, value); -@@ -936,6 +941,11 @@ uint16_t GBAIORead(struct GBA* gba, uint32_t address) { - return 0x1DEA; - } - // Fall through -+ case REG_DEBUG_CYCLES: { -+ int32_t number_of_cycles = mTimingCurrentTime(&gba->timing); -+ mLOG(GBA_DEBUG, INFO, "Cycles: %d", number_of_cycles); -+ return number_of_cycles; -+ } - default: - mLOG(GBA_IO, GAME_ERROR, "Read from unused I/O register: %03X", address); - return GBALoadBad(gba->cpu); diff --git a/mgba-test-runner/build-mgba.sh b/mgba-test-runner/build-mgba.sh deleted file mode 100644 index 5261e541..00000000 --- a/mgba-test-runner/build-mgba.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -MGBA_VERSION=$1 -OUT_DIRECTORY=$2 -CURRENT_DIRECTORY=$(pwd) - -cd "${OUT_DIRECTORY}" || exit - -if [[ ! -f "mgba-${MGBA_VERSION}.tar.gz" ]]; then - curl -L "https://github.com/mgba-emu/mgba/archive/refs/tags/${MGBA_VERSION}.tar.gz" -o "mgba-${MGBA_VERSION}.tar.gz" -fi - -if [[ -f libmgba-cycle.a ]]; then - exit 0 -fi - -curl -L "https://github.com/mgba-emu/mgba/archive/refs/tags/${MGBA_VERSION}.tar.gz" -o "mgba-${MGBA_VERSION}.tar.gz" -tar -xvf "mgba-${MGBA_VERSION}.tar.gz" -cd "mgba-${MGBA_VERSION}" || exit -rm -rf build -patch --strip=1 < "${CURRENT_DIRECTORY}/add_cycles_register.patch" -mkdir -p build -cd build || exit -cmake .. \ - -DBUILD_STATIC=ON \ - -DBUILD_SHARED=OFF \ - -DDISABLE_FRONTENDS=ON \ - -DBUILD_GL=OFF \ - -DBUILD_GLES2=OFF \ - -DUSE_GDB_STUB=OFF \ - -DUSE_FFMPEG=OFF \ - -DUSE_ZLIB=OFF \ - -DUSE_MINIZIP=OFF \ - -DUSE_PNG=OFF \ - -DUSE_LIBZIP=OFF \ - -DUSE_SQLITE3=OFF \ - -DUSE_ELF=ON \ - -DM_CORE_GBA=ON \ - -DM_CORE_GB=OFF \ - -DUSE_LZMA=OFF \ - -DUSE_DISCORD_RPC=OFF \ - -DENABLE_SCRIPTING=OFF \ - -DCMAKE_BUILD_TYPE=Debug \ - -DUSE_EPOXY=OFF -make - -cp libmgba.a ../../libmgba-cycle.a diff --git a/mgba-test-runner/build.rs b/mgba-test-runner/build.rs deleted file mode 100644 index 3679310a..00000000 --- a/mgba-test-runner/build.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{env, path::PathBuf}; - -const MGBA_VERSION: &str = "0.9.1"; - -fn main() { - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - let mgba_directory = out_path.join(format!("mgba-{}", MGBA_VERSION)); - let out = std::process::Command::new("bash") - .arg("build-mgba.sh") - .arg(MGBA_VERSION) - .arg(&out_path) - .output() - .expect("should be able to build mgba"); - if !out.status.success() { - panic!( - "failed to build mgba!\n{}", - String::from_utf8_lossy(&out.stderr), - ); - } - - cc::Build::new() - .file("c/test-runner.c") - .include(&mgba_directory.join("include")) - .static_flag(true) - .debug(true) - .compile("test-runner"); - - println!("cargo:rustc-link-search={}", out_path.to_str().unwrap()); - println!("cargo:rustc-link-lib=static=mgba-cycle"); - println!("cargo:rustc-link-lib=elf"); - - println!("cargo:rerun-if-changed=build-mgba.sh"); - println!("cargo:rerun-if-changed=add_cycles_register.patch"); -} diff --git a/mgba-test-runner/c/test-runner.c b/mgba-test-runner/c/test-runner.c deleted file mode 100644 index 6e50d425..00000000 --- a/mgba-test-runner/c/test-runner.c +++ /dev/null @@ -1,142 +0,0 @@ -#include "test-runner.h" - -#include -#include -#include - -_Static_assert(BYTES_PER_PIXEL == 4, "bytes per pixel MUST be four"); - -void log_output(struct mLogger* _log, int category, enum mLogLevel level, - const char* format, va_list args); -char* log_level_str(enum mLogLevel); - -struct MGBA { - struct mLogger mlogger; - struct mCore* core; - struct video_buffer videoBuffer; - char* filename; - struct callback callback; -}; - -struct MGBA* new_runner(char* filename) { - struct MGBA* mgba = calloc(1, sizeof(struct MGBA)); - mgba->mlogger.log = log_output; - mgba->callback.callback = NULL; - - mLogSetDefaultLogger(&mgba->mlogger); - - char* filename_new = strdup(filename); - mgba->filename = filename_new; - - struct mCore* core = mCoreFind(mgba->filename); - if (!core) { - printf("failed to find core\n"); - free(mgba); - return NULL; - } - - core->init(core); - - unsigned width, height; - core->desiredVideoDimensions(core, &width, &height); - ssize_t videoBufferSize = width * height * BYTES_PER_PIXEL; - - uint32_t* videoBuffer = malloc(videoBufferSize * sizeof(*videoBuffer)); - - core->setVideoBuffer(core, videoBuffer, width); - - // load rom - mCoreLoadFile(core, mgba->filename); - - mCoreConfigInit(&core->config, NULL); - - core->reset(core); - - mgba->core = core; - mgba->videoBuffer = (struct video_buffer){ - .buffer = videoBuffer, - .width = width, - .height = height, - }; - - return mgba; -} - -void set_logger(struct MGBA* mgba, struct callback callback) { - mgba->callback = callback; -} - -void free_runner(struct MGBA* mgba) { - mgba->core->deinit(mgba->core); - mgba->callback.destroy(mgba->callback.data); - free(mgba->filename); - free(mgba->videoBuffer.buffer); - free(mgba); -} - -void advance_frame(struct MGBA* mgba) { mgba->core->runFrame(mgba->core); } - -struct video_buffer get_video_buffer(struct MGBA* mgba) { - return mgba->videoBuffer; -} - -void log_output(struct mLogger* log, int category, enum mLogLevel level, - const char* format, va_list args) { - // cast log to mgba, this works as the logger is the top entry of the mgba - // struct - struct MGBA* mgba = (struct MGBA*)log; - - if (level & 31) { - int32_t size = 0; - - size += snprintf(NULL, 0, "[%s] %s: ", log_level_str(level), - mLogCategoryName(category)); - va_list args_copy; - va_copy(args_copy, args); - size += vsnprintf(NULL, 0, format, args_copy); - va_end(args_copy); - size += 1; - - char* str = calloc(size, sizeof(*str)); - - int32_t offset = snprintf(str, size, "[%s] %s: ", log_level_str(level), - mLogCategoryName(category)); - size -= offset; - vsnprintf(&str[offset], size, format, args); - - if (mgba->callback.callback != NULL) - mgba->callback.callback(mgba->callback.data, str); - else - printf("%s\n", str); - - free(str); - } -} - -char* log_level_str(enum mLogLevel level) { - switch (level) { - case mLOG_FATAL: - return "FATAL"; - break; - case mLOG_ERROR: - return "ERROR"; - break; - case mLOG_WARN: - return "WARNING"; - break; - case mLOG_INFO: - return "INFO"; - break; - case mLOG_DEBUG: - return "DEBUG"; - break; - case mLOG_STUB: - return "STUB"; - break; - case mLOG_GAME_ERROR: - return "GAME ERROR"; - break; - default: - return "Unknown"; - } -} \ No newline at end of file diff --git a/mgba-test-runner/c/test-runner.h b/mgba-test-runner/c/test-runner.h deleted file mode 100644 index b3850296..00000000 --- a/mgba-test-runner/c/test-runner.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -struct MGBA; - -struct video_buffer { - uint32_t width; - uint32_t height; - uint32_t* buffer; -}; - -struct callback { - void* data; - void (*callback)(void*, char[]); - void (*destroy)(void*); -}; - -struct MGBA* new_runner(char filename[]); -void free_runner(struct MGBA* mgba); -void set_logger(struct MGBA*, struct callback); -void advance_frame(struct MGBA* mgba); -struct video_buffer get_video_buffer(struct MGBA* mgba); \ No newline at end of file diff --git a/mgba-test-runner/create-bindings.sh b/mgba-test-runner/create-bindings.sh deleted file mode 100755 index 68f2d09e..00000000 --- a/mgba-test-runner/create-bindings.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -bindgen c/test-runner.h -o src/bindings.rs \ No newline at end of file diff --git a/mgba-test-runner/src/bindings.rs b/mgba-test-runner/src/bindings.rs deleted file mode 100644 index 349f0054..00000000 --- a/mgba-test-runner/src/bindings.rs +++ /dev/null @@ -1,330 +0,0 @@ -/* automatically generated by rust-bindgen 0.63.0 */ - -pub const _STDINT_H: u32 = 1; -pub const _FEATURES_H: u32 = 1; -pub const _DEFAULT_SOURCE: u32 = 1; -pub const __GLIBC_USE_ISOC2X: u32 = 0; -pub const __USE_ISOC11: u32 = 1; -pub const __USE_ISOC99: u32 = 1; -pub const __USE_ISOC95: u32 = 1; -pub const __USE_POSIX_IMPLICITLY: u32 = 1; -pub const _POSIX_SOURCE: u32 = 1; -pub const _POSIX_C_SOURCE: u32 = 200809; -pub const __USE_POSIX: u32 = 1; -pub const __USE_POSIX2: u32 = 1; -pub const __USE_POSIX199309: u32 = 1; -pub const __USE_POSIX199506: u32 = 1; -pub const __USE_XOPEN2K: u32 = 1; -pub const __USE_XOPEN2K8: u32 = 1; -pub const _ATFILE_SOURCE: u32 = 1; -pub const __USE_MISC: u32 = 1; -pub const __USE_ATFILE: u32 = 1; -pub const __USE_FORTIFY_LEVEL: u32 = 0; -pub const __GLIBC_USE_DEPRECATED_GETS: u32 = 0; -pub const __GLIBC_USE_DEPRECATED_SCANF: u32 = 0; -pub const _STDC_PREDEF_H: u32 = 1; -pub const __STDC_IEC_559__: u32 = 1; -pub const __STDC_IEC_559_COMPLEX__: u32 = 1; -pub const __STDC_ISO_10646__: u32 = 201706; -pub const __GNU_LIBRARY__: u32 = 6; -pub const __GLIBC__: u32 = 2; -pub const __GLIBC_MINOR__: u32 = 33; -pub const _SYS_CDEFS_H: u32 = 1; -pub const __glibc_c99_flexarr_available: u32 = 1; -pub const __WORDSIZE: u32 = 64; -pub const __WORDSIZE_TIME64_COMPAT32: u32 = 1; -pub const __SYSCALL_WORDSIZE: u32 = 64; -pub const __LDOUBLE_REDIRECTS_TO_FLOAT128_ABI: u32 = 0; -pub const __HAVE_GENERIC_SELECTION: u32 = 1; -pub const __GLIBC_USE_LIB_EXT2: u32 = 0; -pub const __GLIBC_USE_IEC_60559_BFP_EXT: u32 = 0; -pub const __GLIBC_USE_IEC_60559_BFP_EXT_C2X: u32 = 0; -pub const __GLIBC_USE_IEC_60559_FUNCS_EXT: u32 = 0; -pub const __GLIBC_USE_IEC_60559_FUNCS_EXT_C2X: u32 = 0; -pub const __GLIBC_USE_IEC_60559_TYPES_EXT: u32 = 0; -pub const _BITS_TYPES_H: u32 = 1; -pub const __TIMESIZE: u32 = 64; -pub const _BITS_TYPESIZES_H: u32 = 1; -pub const __OFF_T_MATCHES_OFF64_T: u32 = 1; -pub const __INO_T_MATCHES_INO64_T: u32 = 1; -pub const __RLIM_T_MATCHES_RLIM64_T: u32 = 1; -pub const __STATFS_MATCHES_STATFS64: u32 = 1; -pub const __KERNEL_OLD_TIMEVAL_MATCHES_TIMEVAL64: u32 = 1; -pub const __FD_SETSIZE: u32 = 1024; -pub const _BITS_TIME64_H: u32 = 1; -pub const _BITS_WCHAR_H: u32 = 1; -pub const _BITS_STDINT_INTN_H: u32 = 1; -pub const _BITS_STDINT_UINTN_H: u32 = 1; -pub const INT8_MIN: i32 = -128; -pub const INT16_MIN: i32 = -32768; -pub const INT32_MIN: i32 = -2147483648; -pub const INT8_MAX: u32 = 127; -pub const INT16_MAX: u32 = 32767; -pub const INT32_MAX: u32 = 2147483647; -pub const UINT8_MAX: u32 = 255; -pub const UINT16_MAX: u32 = 65535; -pub const UINT32_MAX: u32 = 4294967295; -pub const INT_LEAST8_MIN: i32 = -128; -pub const INT_LEAST16_MIN: i32 = -32768; -pub const INT_LEAST32_MIN: i32 = -2147483648; -pub const INT_LEAST8_MAX: u32 = 127; -pub const INT_LEAST16_MAX: u32 = 32767; -pub const INT_LEAST32_MAX: u32 = 2147483647; -pub const UINT_LEAST8_MAX: u32 = 255; -pub const UINT_LEAST16_MAX: u32 = 65535; -pub const UINT_LEAST32_MAX: u32 = 4294967295; -pub const INT_FAST8_MIN: i32 = -128; -pub const INT_FAST16_MIN: i64 = -9223372036854775808; -pub const INT_FAST32_MIN: i64 = -9223372036854775808; -pub const INT_FAST8_MAX: u32 = 127; -pub const INT_FAST16_MAX: u64 = 9223372036854775807; -pub const INT_FAST32_MAX: u64 = 9223372036854775807; -pub const UINT_FAST8_MAX: u32 = 255; -pub const UINT_FAST16_MAX: i32 = -1; -pub const UINT_FAST32_MAX: i32 = -1; -pub const INTPTR_MIN: i64 = -9223372036854775808; -pub const INTPTR_MAX: u64 = 9223372036854775807; -pub const UINTPTR_MAX: i32 = -1; -pub const PTRDIFF_MIN: i64 = -9223372036854775808; -pub const PTRDIFF_MAX: u64 = 9223372036854775807; -pub const SIG_ATOMIC_MIN: i32 = -2147483648; -pub const SIG_ATOMIC_MAX: u32 = 2147483647; -pub const SIZE_MAX: i32 = -1; -pub const WINT_MIN: u32 = 0; -pub const WINT_MAX: u32 = 4294967295; -pub type __u_char = ::std::os::raw::c_uchar; -pub type __u_short = ::std::os::raw::c_ushort; -pub type __u_int = ::std::os::raw::c_uint; -pub type __u_long = ::std::os::raw::c_ulong; -pub type __int8_t = ::std::os::raw::c_schar; -pub type __uint8_t = ::std::os::raw::c_uchar; -pub type __int16_t = ::std::os::raw::c_short; -pub type __uint16_t = ::std::os::raw::c_ushort; -pub type __int32_t = ::std::os::raw::c_int; -pub type __uint32_t = ::std::os::raw::c_uint; -pub type __int64_t = ::std::os::raw::c_long; -pub type __uint64_t = ::std::os::raw::c_ulong; -pub type __int_least8_t = __int8_t; -pub type __uint_least8_t = __uint8_t; -pub type __int_least16_t = __int16_t; -pub type __uint_least16_t = __uint16_t; -pub type __int_least32_t = __int32_t; -pub type __uint_least32_t = __uint32_t; -pub type __int_least64_t = __int64_t; -pub type __uint_least64_t = __uint64_t; -pub type __quad_t = ::std::os::raw::c_long; -pub type __u_quad_t = ::std::os::raw::c_ulong; -pub type __intmax_t = ::std::os::raw::c_long; -pub type __uintmax_t = ::std::os::raw::c_ulong; -pub type __dev_t = ::std::os::raw::c_ulong; -pub type __uid_t = ::std::os::raw::c_uint; -pub type __gid_t = ::std::os::raw::c_uint; -pub type __ino_t = ::std::os::raw::c_ulong; -pub type __ino64_t = ::std::os::raw::c_ulong; -pub type __mode_t = ::std::os::raw::c_uint; -pub type __nlink_t = ::std::os::raw::c_ulong; -pub type __off_t = ::std::os::raw::c_long; -pub type __off64_t = ::std::os::raw::c_long; -pub type __pid_t = ::std::os::raw::c_int; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __fsid_t { - pub __val: [::std::os::raw::c_int; 2usize], -} -#[test] -fn bindgen_test_layout___fsid_t() { - const UNINIT: ::std::mem::MaybeUninit<__fsid_t> = ::std::mem::MaybeUninit::uninit(); - let ptr = UNINIT.as_ptr(); - assert_eq!( - ::std::mem::size_of::<__fsid_t>(), - 8usize, - concat!("Size of: ", stringify!(__fsid_t)) - ); - assert_eq!( - ::std::mem::align_of::<__fsid_t>(), - 4usize, - concat!("Alignment of ", stringify!(__fsid_t)) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).__val) as usize - ptr as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(__fsid_t), - "::", - stringify!(__val) - ) - ); -} -pub type __clock_t = ::std::os::raw::c_long; -pub type __rlim_t = ::std::os::raw::c_ulong; -pub type __rlim64_t = ::std::os::raw::c_ulong; -pub type __id_t = ::std::os::raw::c_uint; -pub type __time_t = ::std::os::raw::c_long; -pub type __useconds_t = ::std::os::raw::c_uint; -pub type __suseconds_t = ::std::os::raw::c_long; -pub type __suseconds64_t = ::std::os::raw::c_long; -pub type __daddr_t = ::std::os::raw::c_int; -pub type __key_t = ::std::os::raw::c_int; -pub type __clockid_t = ::std::os::raw::c_int; -pub type __timer_t = *mut ::std::os::raw::c_void; -pub type __blksize_t = ::std::os::raw::c_long; -pub type __blkcnt_t = ::std::os::raw::c_long; -pub type __blkcnt64_t = ::std::os::raw::c_long; -pub type __fsblkcnt_t = ::std::os::raw::c_ulong; -pub type __fsblkcnt64_t = ::std::os::raw::c_ulong; -pub type __fsfilcnt_t = ::std::os::raw::c_ulong; -pub type __fsfilcnt64_t = ::std::os::raw::c_ulong; -pub type __fsword_t = ::std::os::raw::c_long; -pub type __ssize_t = ::std::os::raw::c_long; -pub type __syscall_slong_t = ::std::os::raw::c_long; -pub type __syscall_ulong_t = ::std::os::raw::c_ulong; -pub type __loff_t = __off64_t; -pub type __caddr_t = *mut ::std::os::raw::c_char; -pub type __intptr_t = ::std::os::raw::c_long; -pub type __socklen_t = ::std::os::raw::c_uint; -pub type __sig_atomic_t = ::std::os::raw::c_int; -pub type int_least8_t = __int_least8_t; -pub type int_least16_t = __int_least16_t; -pub type int_least32_t = __int_least32_t; -pub type int_least64_t = __int_least64_t; -pub type uint_least8_t = __uint_least8_t; -pub type uint_least16_t = __uint_least16_t; -pub type uint_least32_t = __uint_least32_t; -pub type uint_least64_t = __uint_least64_t; -pub type int_fast8_t = ::std::os::raw::c_schar; -pub type int_fast16_t = ::std::os::raw::c_long; -pub type int_fast32_t = ::std::os::raw::c_long; -pub type int_fast64_t = ::std::os::raw::c_long; -pub type uint_fast8_t = ::std::os::raw::c_uchar; -pub type uint_fast16_t = ::std::os::raw::c_ulong; -pub type uint_fast32_t = ::std::os::raw::c_ulong; -pub type uint_fast64_t = ::std::os::raw::c_ulong; -pub type intmax_t = __intmax_t; -pub type uintmax_t = __uintmax_t; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct MGBA { - _unused: [u8; 0], -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct video_buffer { - pub width: u32, - pub height: u32, - pub buffer: *mut u32, -} -#[test] -fn bindgen_test_layout_video_buffer() { - const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); - let ptr = UNINIT.as_ptr(); - assert_eq!( - ::std::mem::size_of::(), - 16usize, - concat!("Size of: ", stringify!(video_buffer)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(video_buffer)) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).width) as usize - ptr as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(video_buffer), - "::", - stringify!(width) - ) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).height) as usize - ptr as usize }, - 4usize, - concat!( - "Offset of field: ", - stringify!(video_buffer), - "::", - stringify!(height) - ) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).buffer) as usize - ptr as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(video_buffer), - "::", - stringify!(buffer) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct callback { - pub data: *mut ::std::os::raw::c_void, - pub callback: ::std::option::Option< - unsafe extern "C" fn(arg1: *mut ::std::os::raw::c_void, arg2: *mut ::std::os::raw::c_char), - >, - pub destroy: ::std::option::Option, -} -#[test] -fn bindgen_test_layout_callback() { - const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); - let ptr = UNINIT.as_ptr(); - assert_eq!( - ::std::mem::size_of::(), - 24usize, - concat!("Size of: ", stringify!(callback)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(callback)) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).data) as usize - ptr as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(callback), - "::", - stringify!(data) - ) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).callback) as usize - ptr as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(callback), - "::", - stringify!(callback) - ) - ); - assert_eq!( - unsafe { ::std::ptr::addr_of!((*ptr).destroy) as usize - ptr as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(callback), - "::", - stringify!(destroy) - ) - ); -} -extern "C" { - pub fn new_runner(filename: *mut ::std::os::raw::c_char) -> *mut MGBA; -} -extern "C" { - pub fn free_runner(mgba: *mut MGBA); -} -extern "C" { - pub fn set_logger(arg1: *mut MGBA, arg2: callback); -} -extern "C" { - pub fn advance_frame(mgba: *mut MGBA); -} -extern "C" { - pub fn get_video_buffer(mgba: *mut MGBA) -> video_buffer; -} diff --git a/mgba-test-runner/src/main.rs b/mgba-test-runner/src/main.rs deleted file mode 100644 index a733b4ee..00000000 --- a/mgba-test-runner/src/main.rs +++ /dev/null @@ -1,196 +0,0 @@ -#![allow(clippy::all)] - -mod bindings; -mod runner; - -use anyhow::{anyhow, Error}; -use image::io::Reader; -use image::GenericImage; -use io::Write; -use regex::Regex; -use runner::VideoBuffer; -use std::cell::Cell; -use std::io; -use std::path::Path; -use std::rc::Rc; - -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -enum Status { - Running, - Failed, - Success, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum Timing { - None, - WaitFor(i32), - Difference(i32), -} - -const TEST_RUNNER_TAG: u16 = 785; - -fn test_file(file_to_run: &str) -> Status { - let finished = Rc::new(Cell::new(Status::Running)); - let debug_reader_mutex = Regex::new(r"(?s)^\[(.*)\] GBA Debug: (.*)$").unwrap(); - let tagged_cycles_reader = Regex::new(r"Cycles: (\d*) Tag: (\d*)").unwrap(); - - let mut mgba = runner::MGBA::new(file_to_run).unwrap(); - - { - let finished = finished.clone(); - let video_buffer = mgba.get_video_buffer(); - let number_of_cycles = Cell::new(Timing::None); - - mgba.set_logger(move |message| { - if let Some(captures) = debug_reader_mutex.captures(message) { - let log_level = &captures[1]; - let out = &captures[2]; - - if out.starts_with("image:") { - let image_path = out.strip_prefix("image:").unwrap(); - match check_image_match(image_path, &video_buffer) { - Err(e) => { - println!("[failed]"); - println!("{}", e); - finished.set(Status::Failed); - } - Ok(_) => {} - } - } else if out.ends_with("...") { - print!("{}", out); - io::stdout().flush().expect("can't flush stdout"); - } else if out.starts_with("Cycles: ") { - if let Some(captures) = tagged_cycles_reader.captures(out) { - let num_cycles: i32 = captures[1].parse().unwrap(); - let tag: u16 = captures[2].parse().unwrap(); - - if tag == TEST_RUNNER_TAG { - number_of_cycles.set(match number_of_cycles.get() { - Timing::WaitFor(n) => Timing::Difference(num_cycles - n), - Timing::None => Timing::WaitFor(num_cycles), - Timing::Difference(_) => Timing::WaitFor(num_cycles), - }); - } - } - } else if out == "[ok]" { - if let Timing::Difference(cycles) = number_of_cycles.get() { - println!( - "[ok: {} c ≈ {} s]", - cycles, - ((cycles as f64 / (16.78 * 1_000_000.0)) * 100.0).round() / 100.0 - ); - } else { - println!("{}", out); - } - } else { - println!("{}", out); - } - - if log_level == "FATAL" { - finished.set(Status::Failed); - } - - if out == "Tests finished successfully" { - finished.set(Status::Success); - } - } - }); - } - - loop { - mgba.advance_frame(); - if finished.get() != Status::Running { - break; - } - } - - return finished.get(); -} - -fn main() -> Result<(), Error> { - let args: Vec = std::env::args().collect(); - let file_to_run = args.get(1).expect("you should provide file to run"); - - if !Path::new(file_to_run).exists() { - return Err(anyhow!("File to run should exist!")); - } - - let output = test_file(file_to_run); - - match output { - Status::Failed => Err(anyhow!("Tests failed!")), - Status::Success => Ok(()), - _ => { - unreachable!("very bad thing happened"); - } - } -} - -fn gba_colour_to_rgba(colour: u32) -> [u8; 4] { - [ - ((colour >> 0) & 0xFF) as u8, - ((colour >> 8) & 0xFF) as u8, - ((colour >> 16) & 0xFF) as u8, - 255, - ] -} - -fn rgba_to_gba_to_rgba(c: [u8; 4]) -> [u8; 4] { - let mut n = c.clone(); - n.iter_mut() - .for_each(|a| *a = ((((*a as u32 >> 3) << 3) * 0x21) >> 5) as u8); - n -} - -fn check_image_match(image_path: &str, video_buffer: &VideoBuffer) -> Result<(), Error> { - let expected_image = Reader::open(image_path)?.decode()?; - let expected = expected_image.to_rgba8(); - - let (buf_dim_x, buf_dim_y) = video_buffer.get_size(); - let (exp_dim_x, exp_dim_y) = expected.dimensions(); - if (buf_dim_x != exp_dim_x) || (buf_dim_y != exp_dim_y) { - return Err(anyhow!("image sizes do not match")); - } - - for y in 0..buf_dim_y { - for x in 0..buf_dim_x { - let video_pixel = video_buffer.get_pixel(x, y); - let image_pixel = expected.get_pixel(x, y); - let video_pixel = gba_colour_to_rgba(video_pixel); - let image_pixel = rgba_to_gba_to_rgba(image_pixel.0); - if image_pixel != video_pixel { - let output_file = write_video_buffer(video_buffer); - - return Err(anyhow!( - "images do not match, actual output written to {}", - output_file - )); - } - } - } - - Ok(()) -} - -fn write_video_buffer(video_buffer: &VideoBuffer) -> String { - let (width, height) = video_buffer.get_size(); - let mut output_image = image::DynamicImage::new_rgba8(width, height); - - for y in 0..height { - for x in 0..width { - let pixel = video_buffer.get_pixel(x, y); - let pixel_as_rgba = gba_colour_to_rgba(pixel); - - output_image.put_pixel(x, y, pixel_as_rgba.into()) - } - } - - let output_folder = std::env::temp_dir(); - let output_file = "mgba-test-runner-output.png"; // TODO make this random - - let output_file = output_folder.join(output_file); - let _ = output_image.save_with_format(&output_file, image::ImageFormat::Png); - - output_file.to_string_lossy().into_owned() -} diff --git a/mgba-test-runner/src/runner.rs b/mgba-test-runner/src/runner.rs deleted file mode 100644 index 85a1640f..00000000 --- a/mgba-test-runner/src/runner.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::bindings; - -use std::ffi::c_void; -use std::ffi::CStr; -use std::ffi::CString; -use std::marker::PhantomData; -use std::os::raw::c_char; - -#[allow( - non_upper_case_globals, - dead_code, - non_camel_case_types, - non_snake_case -)] - -pub struct MGBA<'a> { - mgba: *mut bindings::MGBA, - _phantom: PhantomData<&'a ()>, -} - -pub struct VideoBuffer { - width: u32, - height: u32, - buffer: *mut u32, -} - -impl VideoBuffer { - pub fn get_size(&self) -> (u32, u32) { - (self.width, self.height) - } - pub fn get_pixel(&self, x: u32, y: u32) -> u32 { - let offset = (y * self.width + x) as isize; - assert!(x < self.width, "x must be in range 0 to {}", self.width); - assert!(y < self.height, "y must be in range 0 to {}", self.height); - unsafe { *self.buffer.offset(offset) } - } -} - -impl<'a> MGBA<'a> { - pub fn new(filename: &str) -> Result { - let c_str = CString::new(filename).expect("should be able to make cstring from filename"); - let mgba = unsafe { bindings::new_runner(c_str.as_ptr() as *mut c_char) }; - if mgba.is_null() { - Err(anyhow::anyhow!("could not create core")) - } else { - Ok(MGBA { - mgba, - _phantom: PhantomData, - }) - } - } - - pub fn get_video_buffer(&self) -> VideoBuffer { - let c_video_buffer = unsafe { bindings::get_video_buffer(self.mgba) }; - VideoBuffer { - width: c_video_buffer.width, - height: c_video_buffer.height, - buffer: c_video_buffer.buffer, - } - } - - pub fn advance_frame(&mut self) { - unsafe { bindings::advance_frame(self.mgba) } - } - pub fn set_logger(&mut self, logger: impl Fn(&str) + 'a) { - unsafe { - let callback = generate_c_callback(move |message: *mut c_char| { - logger( - CStr::from_ptr(message) - .to_str() - .expect("should be able to convert logging message to rust String"), - ); - }); - bindings::set_logger(self.mgba, callback) - } - } -} - -unsafe fn generate_c_callback(f: F) -> bindings::callback -where - F: FnMut(*mut c_char), -{ - let data = Box::into_raw(Box::new(f)); - - bindings::callback { - callback: Some(call_closure::), - data: data as *mut _, - destroy: Some(drop_box::), - } -} - -extern "C" fn call_closure(data: *mut c_void, message: *mut c_char) -where - F: FnMut(*mut c_char), -{ - let callback_ptr = data as *mut F; - let callback = unsafe { &mut *callback_ptr }; - callback(message); -} - -extern "C" fn drop_box(data: *mut c_void) { - unsafe { - Box::from_raw(data as *mut T); - } -} - -impl Drop for MGBA<'_> { - fn drop(&mut self) { - unsafe { bindings::free_runner(self.mgba) } - } -} From c75ddf09428060afc48be6d4cfa26ea9e6fec763 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 12:18:59 +0100 Subject: [PATCH 02/12] fix path --- emulator/test-runner/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emulator/test-runner/Cargo.toml b/emulator/test-runner/Cargo.toml index e35364d5..65889bee 100644 --- a/emulator/test-runner/Cargo.toml +++ b/emulator/test-runner/Cargo.toml @@ -10,4 +10,4 @@ mgba = { path = "../mgba" } clap = { version = "4.3", features = ["derive"] } anyhow = "1.0" image = { version = "0.24", default-features = false, features = [ "png", "bmp" ] } -agb-gbafix = { path = "../../agb/agb-gbafix" } \ No newline at end of file +agb-gbafix = { path = "../../agb-gbafix" } \ No newline at end of file From f873ca2c6b82d719fbbefa23f9c99cc0e8816917 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 12:19:07 +0100 Subject: [PATCH 03/12] remove pointless ignore --- emulator/.gitignore | 1 - 1 file changed, 1 deletion(-) delete mode 100644 emulator/.gitignore diff --git a/emulator/.gitignore b/emulator/.gitignore deleted file mode 100644 index ea8c4bf7..00000000 --- a/emulator/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target From 59efa2922528c753ce023992f66f92bda3a44d52 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 12:57:35 +0100 Subject: [PATCH 04/12] include mgba's include directory --- emulator/mgba-sys/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/emulator/mgba-sys/build.rs b/emulator/mgba-sys/build.rs index 35a543e9..823635f8 100644 --- a/emulator/mgba-sys/build.rs +++ b/emulator/mgba-sys/build.rs @@ -52,6 +52,7 @@ fn generate_bindings() -> MyError { .allowlist_function("mLogCategoryName") .generate_cstr(true) .derive_default(true) + .clang_arg("-I./mgba/include") .generate()?; let out_path = PathBuf::from(env::var("OUT_DIR")?); From d4c1dc1730ffacca383b51f37a879abc4bb005d1 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 13:11:06 +0100 Subject: [PATCH 05/12] use same target for everything --- .github/workflows/build-and-test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 2f05c394..5b328e86 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -16,6 +16,8 @@ jobs: name: Just CI runs-on: ubuntu-20.04 steps: + - name: Set CARGO_TARGET_DIR + run: echo "CARGO_TARGET_DIR=$HOME/target" >> $GITHUB_ENV - name: Install build tools run: sudo apt-get update && sudo apt-get install build-essential libelf-dev zip -y - name: Install Miri @@ -31,13 +33,9 @@ jobs: ~/.cargo/registry ~/.cargo/git ~/target - emulator/target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: install mgba-test-runner run: cargo install --path emulator/test-runner --verbose - - name: Set CARGO_TARGET_DIR - run: echo "CARGO_TARGET_DIR=$HOME/target" >> $GITHUB_ENV - uses: extractions/setup-just@v1 - name: Setup mdBook uses: peaceiris/actions-mdbook@v1 From 3430668bdd67a38d85ca8f56a32866721d4ad773 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 13:11:22 +0100 Subject: [PATCH 06/12] change name of cycle measure function and remove from public api --- agb/src/lib.rs | 4 ++-- agb/src/mgba.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 063090e2..c4e57618 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -289,9 +289,9 @@ pub mod test_runner { mgba::DebugLevel::Info, ) .unwrap(); - mgba::number_of_cycles_tagged(785); + mgba::test_runner_measure_cycles(); self(gba); - mgba::number_of_cycles_tagged(785); + mgba::test_runner_measure_cycles(); assert!( unsafe { agb_alloc::number_of_blocks() } < 2, diff --git a/agb/src/mgba.rs b/agb/src/mgba.rs index 5104fd7d..348df783 100644 --- a/agb/src/mgba.rs +++ b/agb/src/mgba.rs @@ -28,8 +28,8 @@ fn is_running_in_mgba() -> bool { const NUMBER_OF_CYCLES: MemoryMapped = unsafe { MemoryMapped::new(0x04FF_F800) }; -pub fn number_of_cycles_tagged(tag: u16) { - NUMBER_OF_CYCLES.set(tag); +pub(crate) fn test_runner_measure_cycles() { + NUMBER_OF_CYCLES.set(0); } pub struct Mgba { From 987928a9a8966bca77d990484492c7678aa90b73 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 19:14:49 +0100 Subject: [PATCH 07/12] initialise to the no logger iff no other logger has been loaded --- emulator/mgba/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/emulator/mgba/src/lib.rs b/emulator/mgba/src/lib.rs index 9f9d8777..2757d664 100644 --- a/emulator/mgba/src/lib.rs +++ b/emulator/mgba/src/lib.rs @@ -1,7 +1,7 @@ mod log; mod vfile; -use std::ptr::NonNull; +use std::{ptr::NonNull, sync::atomic::{AtomicBool, Ordering}}; pub use log::{Logger, LogLevel}; pub use vfile::{file::FileBacked, memory::MemoryBacked, shared::Shared, MapFlag, VFile}; @@ -27,13 +27,18 @@ macro_rules! call_on_core { }; } +static GLOBAL_LOGGER_HAS_BEEN_INITIALISED: AtomicBool = AtomicBool::new(false); + pub fn set_global_default_logger(logger: &'static Logger) { + GLOBAL_LOGGER_HAS_BEEN_INITIALISED.store(true, Ordering::SeqCst); unsafe { mgba_sys::mLogSetDefaultLogger(logger.to_mgba()) } } impl MCore { pub fn new() -> Option { - set_global_default_logger(&log::NO_LOGGER); + if !GLOBAL_LOGGER_HAS_BEEN_INITIALISED.load(Ordering::SeqCst) { + set_global_default_logger(&log::NO_LOGGER); + } let core = unsafe { mgba_sys::GBACoreCreate() }; let core = NonNull::new(core)?; From 246e4da99e6351adf2f1adf19129c1df18af97c9 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 5 Aug 2023 19:15:06 +0100 Subject: [PATCH 08/12] run tests for emulator workspace --- justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/justfile b/justfile index 1e3f339e..1cbabf36 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,7 @@ test: just _test-debug tracker/agb-tracker just _test-debug-arm agb just _test-debug tools + just _test-debug emulator test-release: just _test-release agb From 29c7ee992984cf1183c5f292102287ff63bf713d Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 8 Aug 2023 09:33:36 +0100 Subject: [PATCH 09/12] change emulator directory notes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5bead75..f4bef21e 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ for performant decimals. `examples` - bigger examples of a complete game, made during game jams -`mgba-test-runner` - a wrapper around the [mgba](https://mgba.io) emulator which allows us to write unit tests in rust +`emulator` - Rust bindings for the [mgba](https://mgba.io) emulator used for our purposes. Currently this does not accept contributions. `template` - the source for the [template repository](https://github.com/agbrs/template) From ad1f70e6e50d28edba27c8e890104fa7a3ce9ace Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 8 Aug 2023 09:34:58 +0100 Subject: [PATCH 10/12] use less specific versions of clap and anyhow --- emulator/test-runner/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/emulator/test-runner/Cargo.toml b/emulator/test-runner/Cargo.toml index 65889bee..e86d5aec 100644 --- a/emulator/test-runner/Cargo.toml +++ b/emulator/test-runner/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] mgba = { path = "../mgba" } -clap = { version = "4.3", features = ["derive"] } -anyhow = "1.0" +clap = { version = "4", features = ["derive"] } +anyhow = "1" image = { version = "0.24", default-features = false, features = [ "png", "bmp" ] } agb-gbafix = { path = "../../agb-gbafix" } \ No newline at end of file From d9a0ab1a9f830d6ffda16d3656eb0df83762190f Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 8 Aug 2023 09:44:29 +0100 Subject: [PATCH 11/12] make non rust trait parts of vfile private --- emulator/mgba/src/vfile.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/emulator/mgba/src/vfile.rs b/emulator/mgba/src/vfile.rs index c6ac8e95..97092b3f 100644 --- a/emulator/mgba/src/vfile.rs +++ b/emulator/mgba/src/vfile.rs @@ -13,7 +13,7 @@ pub enum MapFlag { Write, } -pub trait VFile: Seek + Read + Write { +trait VFileExtensions: VFile { fn readline(&mut self, buffer: &mut [u8]) -> Result { let mut byte = 0; while byte < buffer.len() - 1 { @@ -92,6 +92,10 @@ pub trait VFile: Seek + Read + Write { } } +pub trait VFile: Seek + Read + Write {} + +impl VFileExtensions for T {} + #[repr(C)] struct VFileInner { vfile: mgba_sys::VFile, @@ -116,6 +120,7 @@ impl VFileAlloc { mod vfile_extern { use std::io::SeekFrom; + use super::VFileExtensions; /// Safety: Must be part of a VFileInner pub unsafe fn create_vfile() -> mgba_sys::VFile { From 5eb240068a226ebae53e989c3e07a0ba9596d4c9 Mon Sep 17 00:00:00 2001 From: Corwin Date: Wed, 9 Aug 2023 21:06:54 +0100 Subject: [PATCH 12/12] video buffer using unsafe cell across ffi boundary --- emulator/mgba/src/lib.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/emulator/mgba/src/lib.rs b/emulator/mgba/src/lib.rs index 2757d664..a1b68525 100644 --- a/emulator/mgba/src/lib.rs +++ b/emulator/mgba/src/lib.rs @@ -1,16 +1,20 @@ mod log; mod vfile; -use std::{ptr::NonNull, sync::atomic::{AtomicBool, Ordering}}; +use std::{ + cell::UnsafeCell, + ptr::NonNull, + sync::atomic::{AtomicBool, Ordering}, +}; -pub use log::{Logger, LogLevel}; +pub use log::{LogLevel, Logger}; pub use vfile::{file::FileBacked, memory::MemoryBacked, shared::Shared, MapFlag, VFile}; use vfile::VFileAlloc; pub struct MCore { core: NonNull, - video_buffer: Box<[u32]>, + video_buffer: UnsafeCell>, } impl Drop for MCore { @@ -51,17 +55,18 @@ impl MCore { unsafe { call_on_core!(core=>desiredVideoDimensions(&mut width, &mut height)) }; - let mut video_buffer = vec![ - 0; - (width * height * mgba_sys::BYTES_PER_PIXEL) as usize - / std::mem::size_of::() - ] - .into_boxed_slice(); + let mut video_buffer = UnsafeCell::new( + vec![ + 0; + (width * height * mgba_sys::BYTES_PER_PIXEL) as usize / std::mem::size_of::() + ] + .into_boxed_slice(), + ); unsafe { call_on_core!( core=>setVideoBuffer( - video_buffer.as_mut_ptr(), + video_buffer.get_mut().as_mut_ptr(), width as usize ) ) @@ -123,7 +128,9 @@ impl MCore { } pub fn video_buffer(&mut self) -> &[u32] { - self.video_buffer.as_ref() + // Safety: For the duration of this borrow, mgba can't be called into, + // so this reference can be taken. + unsafe { &*self.video_buffer.get() } } pub fn current_cycle(&mut self) -> u64 {