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 00000000..84c9fe4a Binary files /dev/null and b/emulator/mgba/save.gba differ 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 00000000..29b7211a Binary files /dev/null and b/emulator/mgba/test.gba differ diff --git a/emulator/test-runner/Cargo.toml b/emulator/test-runner/Cargo.toml new file mode 100644 index 00000000..e35364d5 --- /dev/null +++ b/emulator/test-runner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mgba-test-runner" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +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 diff --git a/emulator/test-runner/src/image_compare.rs b/emulator/test-runner/src/image_compare.rs new file mode 100644 index 00000000..413d2e00 --- /dev/null +++ b/emulator/test-runner/src/image_compare.rs @@ -0,0 +1,64 @@ +use std::path::Path; + +use image::{io::Reader, DynamicImage}; + +pub struct ComparisonResult { + matches: bool, + image: DynamicImage, +} + +impl ComparisonResult { + pub fn success(&self) -> 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) } - } -}