replace mgba-test-runner with better bindings

This commit is contained in:
Corwin 2023-08-05 12:16:52 +01:00
parent 5fb928adb8
commit 4acf98bcbd
No known key found for this signature in database
34 changed files with 1903 additions and 1173 deletions

View file

@ -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

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "emulator/mgba-sys/mgba"]
path = emulator/mgba-sys/mgba
url = https://github.com/mgba-emu/mgba.git

View file

@ -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)

1
emulator/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

731
emulator/Cargo.lock generated Normal file
View file

@ -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"

6
emulator/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[workspace]
members = [
"mgba",
"mgba-sys",
"test-runner"
]

View file

@ -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"

View file

@ -0,0 +1,76 @@
use std::{
env,
error::Error,
path::{Path, PathBuf},
process::Command,
};
type MyError = Result<(), Box<dyn Error>>;
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(())
}

@ -0,0 +1 @@
Subproject commit 2fb55450610a4d10479a1eed1408f905d79318e3

View file

@ -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 {}

View file

@ -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"

11
emulator/mgba/Cargo.toml Normal file
View file

@ -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"

BIN
emulator/mgba/save.gba Normal file

Binary file not shown.

216
emulator/mgba/src/lib.rs Normal file
View file

@ -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<mgba_sys::mCore>,
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<Self> {
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::<u32>()
]
.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<V: VFile>(&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<V: VFile>(&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::<Vec<u8>>(),
"First 128 bytes should be ascending numbers"
);
assert_eq!(
save_file[128..],
std::iter::repeat(0xFF)
.take(save_file.len() - 128)
.collect::<Vec<u8>>(),
"Remanider of save should be 0xFF, all ones"
);
}
}

143
emulator/mgba/src/log.rs Normal file
View file

@ -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<mgba_sys::mLogLevel> for LogLevel {
type Error = LogLevelIsNotValid;
fn try_from(value: mgba_sys::mLogLevel) -> Result<Self, LogLevelIsNotValid> {
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<fn(&str, LogLevel, String)>,
}
impl Logger {
pub(crate) fn to_mgba(&'static self) -> *mut mgba_sys::mLogger {
(self as *const Logger)
.cast::<mgba_sys::mLogger>()
.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::<Logger>();
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<String> {
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,
) {
}

266
emulator/mgba/src/vfile.rs Normal file
View file

@ -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<usize> {
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<Box<[u8]>> {
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<u8> = 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<usize> {
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<V: VFile> {
vfile: mgba_sys::VFile,
file: V,
}
pub struct VFileAlloc<V: VFile>(Box<VFileInner<V>>);
impl<V: VFile> VFileAlloc<V> {
pub fn new(f: V) -> Self {
Self(Box::new(VFileInner {
vfile: unsafe { vfile_extern::create_vfile::<V>() },
file: f,
}))
}
pub(crate) fn into_mgba(self) -> *mut mgba_sys::VFile {
let f = Box::into_raw(self.0) as *mut VFileInner<V>;
f.cast()
}
}
mod vfile_extern {
use std::io::SeekFrom;
/// Safety: Must be part of a VFileInner
pub unsafe fn create_vfile<V: super::VFile>() -> mgba_sys::VFile {
mgba_sys::VFile {
close: Some(close::<V>),
seek: Some(seek::<V>),
read: Some(read::<V>),
readline: Some(readline::<V>),
write: Some(write::<V>),
map: Some(map::<V>),
unmap: Some(unmap::<V>),
truncate: Some(truncate::<V>),
size: Some(size::<V>),
sync: Some(sync::<V>),
}
}
use mgba_sys::VFile;
extern "C" fn close<V: super::VFile>(vf: *mut VFile) -> bool {
drop(unsafe { Box::from_raw(vf.cast::<super::VFileInner<V>>()) });
true
}
unsafe fn with_inner<V: super::VFile, F, T>(vf: *mut VFile, f: F) -> T
where
F: FnOnce(&mut dyn super::VFile) -> T,
{
let vf = vf.cast::<super::VFileInner<V>>();
let vf = &mut *vf;
f(&mut vf.file)
}
extern "C" fn seek<V: super::VFile>(
vf: *mut VFile,
offset: mgba_sys::off_t,
whence: std::os::raw::c_int,
) -> mgba_sys::off_t {
unsafe {
with_inner::<V, _, _>(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<V: super::VFile>(
vf: *mut VFile,
buffer: *mut ::std::os::raw::c_void,
size: usize,
) -> isize {
unsafe {
with_inner::<V, _, _>(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<V: super::VFile>(
vf: *mut VFile,
buffer: *mut ::std::os::raw::c_char,
size: usize,
) -> isize {
unsafe {
with_inner::<V, _, _>(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<V: super::VFile>(
vf: *mut VFile,
buffer: *const ::std::os::raw::c_void,
size: usize,
) -> isize {
unsafe {
with_inner::<V, _, _>(vf, |vf| {
vf.write(std::slice::from_raw_parts(buffer.cast(), size))
.map(|x| x as isize)
.unwrap_or(-1)
})
}
}
extern "C" fn map<V: super::VFile>(
vf: *mut VFile,
size: usize,
flags: ::std::os::raw::c_int,
) -> *mut ::std::os::raw::c_void {
unsafe {
with_inner::<V, _, _>(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<V: super::VFile>(
vf: *mut VFile,
memory: *mut ::std::os::raw::c_void,
size: usize,
) {
unsafe {
with_inner::<V, _, _>(vf, |vf| {
let b = Box::from_raw(std::slice::from_raw_parts_mut(memory.cast::<u8>(), size));
let _ = vf.unmap(b);
})
}
}
extern "C" fn truncate<V: super::VFile>(vf: *mut VFile, size: usize) {
unsafe {
let _ = with_inner::<V, _, _>(vf, |vf| vf.truncate(size));
}
}
extern "C" fn size<V: super::VFile>(vf: *mut VFile) -> isize {
unsafe { with_inner::<V, _, _>(vf, |vf| vf.size().map(|x| x as isize).unwrap_or(-1)) }
}
extern "C" fn sync<V: super::VFile>(
vf: *mut VFile,
buffer: *mut ::std::os::raw::c_void,
size: usize,
) -> bool {
unsafe {
with_inner::<V, _, _>(vf, |vf| {
vf.sync(std::slice::from_raw_parts(buffer.cast(), size))
.is_ok()
})
}
}
}

View file

@ -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<u64> {
self.file.seek(pos)
}
}
impl Write for FileBacked {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<usize> {
self.file.read(buf)
}
}
impl VFile for FileBacked {}

View file

@ -0,0 +1,61 @@
use std::{
borrow::Cow,
io::{Cursor, Read, Seek, Write},
};
use super::VFile;
pub struct MemoryBacked {
buffer: Cursor<Cow<'static, [u8]>>,
}
impl MemoryBacked {
pub fn new_from_slice(data: &'static [u8]) -> Self {
Self {
buffer: Cursor::new(Cow::Borrowed(data)),
}
}
pub fn new(data: Vec<u8>) -> 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<usize> {
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<usize> {
self.buffer.read(buf)
}
}
impl Seek for MemoryBacked {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.buffer.seek(pos)
}
}
impl VFile for MemoryBacked {}

View file

@ -0,0 +1,64 @@
use std::{
io::{Read, Seek, Write},
sync::{Arc, Mutex},
};
use super::VFile;
pub struct Shared<V> {
inner: Arc<Mutex<V>>,
}
impl<V> Clone for Shared<V> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<V> Shared<V> {
pub fn new(v: V) -> Self {
Self {
inner: Arc::new(Mutex::new(v)),
}
}
pub fn try_into_inner(self) -> Result<V, Self> {
Arc::try_unwrap(self.inner)
.map(|x| x.into_inner().unwrap())
.map_err(|e| Self { inner: e })
}
}
impl<V: Clone> Shared<V> {
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<V: Write> Write for Shared<V> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.inner.lock().unwrap().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.lock().unwrap().flush()
}
}
impl<V: Read> Read for Shared<V> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.lock().unwrap().read(buf)
}
}
impl<V: Seek> Seek for Shared<V> {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.lock().unwrap().seek(pos)
}
}
impl<V: VFile> VFile for Shared<V> {}

BIN
emulator/mgba/test.gba Normal file

Binary file not shown.

View file

@ -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" }

View file

@ -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<Path>,
video_buffer: &[u32],
) -> anyhow::Result<ComparisonResult> {
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,
})
}

View file

@ -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<VecDeque<(String, LogLevel, String)>> = 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<V: VFile>(rom: V) -> Result<Self, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
let args = CliArguments::parse();
let rom = load_rom(args.rom)?;
let rom = MemoryBacked::new(rom);
TestRunner::new(rom)?.run()?;
Ok(())
}
fn load_rom<P: AsRef<Path>>(path: P) -> anyhow::Result<Vec<u8>> {
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)
}
}

View file

@ -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"

View file

@ -1,13 +0,0 @@
[package]
name = "mgba-test-runner"
version = "0.1.0"
authors = ["Corwin Kuiper <corwin@kuiper.dev>"]
edition = "2021"
[dependencies]
regex = "1"
anyhow = "1"
image = { version = "0.24", default-features = false, features = [ "png", "bmp" ] }
[build-dependencies]
cc = { version = "1", features = ["parallel"] }

View file

@ -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);

View file

@ -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

View file

@ -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");
}

View file

@ -1,142 +0,0 @@
#include "test-runner.h"
#include <mgba/core/core.h>
#include <mgba/feature/commandline.h>
#include <stdio.h>
_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";
}
}

View file

@ -1,23 +0,0 @@
#pragma once
#include <stdint.h>
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);

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
bindgen c/test-runner.h -o src/bindings.rs

View file

@ -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<video_buffer> = ::std::mem::MaybeUninit::uninit();
let ptr = UNINIT.as_ptr();
assert_eq!(
::std::mem::size_of::<video_buffer>(),
16usize,
concat!("Size of: ", stringify!(video_buffer))
);
assert_eq!(
::std::mem::align_of::<video_buffer>(),
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<unsafe extern "C" fn(arg1: *mut ::std::os::raw::c_void)>,
}
#[test]
fn bindgen_test_layout_callback() {
const UNINIT: ::std::mem::MaybeUninit<callback> = ::std::mem::MaybeUninit::uninit();
let ptr = UNINIT.as_ptr();
assert_eq!(
::std::mem::size_of::<callback>(),
24usize,
concat!("Size of: ", stringify!(callback))
);
assert_eq!(
::std::mem::align_of::<callback>(),
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;
}

View file

@ -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<String> = 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()
}

View file

@ -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<Self, anyhow::Error> {
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: F) -> bindings::callback
where
F: FnMut(*mut c_char),
{
let data = Box::into_raw(Box::new(f));
bindings::callback {
callback: Some(call_closure::<F>),
data: data as *mut _,
destroy: Some(drop_box::<F>),
}
}
extern "C" fn call_closure<F>(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<T>(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) }
}
}