Initial commit: NES emulator with GTK4 desktop frontend
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
Full NES emulation: CPU, PPU, APU, 47 mappers, iNES/NES 2.0 parsing. GTK4 desktop client with HeaderBar, pixel-perfect Cairo rendering, drag-and-drop ROM loading, and keyboard shortcuts. 187 tests covering core emulation, mappers, and runtime.
This commit is contained in:
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[alias]
|
||||||
|
clippy-strict = "clippy --all-targets --all-features -- -D warnings"
|
||||||
|
clippy-relaxed = "clippy --all-targets --all-features -- -W clippy::pedantic -W clippy::wildcard_imports -W clippy::match_same_arms"
|
||||||
35
.github/workflows/ci.yml
vendored
Normal file
35
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rust:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install stable toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --locked
|
||||||
|
|
||||||
|
- name: Format
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
- name: Clippy (strict)
|
||||||
|
run: cargo clippy-strict
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --all-targets --all-features
|
||||||
|
|
||||||
|
- name: Public API Contract Tests
|
||||||
|
run: cargo test --test public_api --all-features
|
||||||
|
|
||||||
|
- name: Minimal Client Contract Tests
|
||||||
|
run: cargo test -p nesemu-client-minimal --test cli_contract
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
784
Cargo.lock
generated
Normal file
784
Cargo.lock
generated
Normal file
@@ -0,0 +1,784 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cairo-rs"
|
||||||
|
version = "0.19.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2ac2a4d0e69036cf0062976f6efcba1aaee3e448594e6514bb2ddf87acce562"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cairo-sys-rs"
|
||||||
|
version = "0.19.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd3bb3119664efbd78b5e6c93957447944f16bdbced84c17a9f41c7829b81e64"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-expr"
|
||||||
|
version = "0.15.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"target-lexicon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "field-offset"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
|
||||||
|
dependencies = [
|
||||||
|
"memoffset",
|
||||||
|
"rustc_version",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk-pixbuf"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "624eaba126021103c7339b2e179ae4ee8cdab842daab419040710f38ed9f8699"
|
||||||
|
dependencies = [
|
||||||
|
"gdk-pixbuf-sys",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk-pixbuf-sys"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4efa05a4f83c8cc50eb4d883787b919b85e5f1d8dd10b5a1df53bf5689782379"
|
||||||
|
dependencies = [
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk4"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db265c9dd42d6a371e09e52deab3a84808427198b86ac792d75fd35c07990a07"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gdk-pixbuf",
|
||||||
|
"gdk4-sys",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gdk4-sys"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9418fb4e8a67074919fe7604429c45aa74eb9df82e7ca529767c6d4e9dc66dd"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"gdk-pixbuf-sys",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"pkg-config",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gio"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c49f117d373ffcc98a35d114db5478bc223341cff53e39a5d6feced9e2ddffe"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"gio-sys",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gio-sys"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2cd743ba4714d671ad6b6234e8ab2a13b42304d0e13ab7eba1dcdd78a7d6d4ef"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib"
|
||||||
|
version = "0.19.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-macros",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"memchr",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-macros"
|
||||||
|
version = "0.19.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4429b0277a14ae9751350ad9b658b1be0abb5b54faa5bcdf6e74a3372582fad7"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glib-sys"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c2dc18d3a82b0006d470b13304fbbb3e0a9bd4884cf985a60a7ed733ac2c4a5"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gobject-sys"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e697e252d6e0416fd1d9e169bda51c0f1c926026c39ca21fbe8b1bb5c3b8b9e"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphene-rs"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f5fb86031d24d9ec0a2a15978fc7a65d545a2549642cf1eb7c3dda358da42bcf"
|
||||||
|
dependencies = [
|
||||||
|
"glib",
|
||||||
|
"graphene-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphene-sys"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f530e0944bccba4b55065e9c69f4975ad691609191ebac16e13ab8e1f27af05"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gsk4"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7563884bf6939f4468e5d94654945bdd9afcaf8c3ba4c5dd17b5342b747221be"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gdk4",
|
||||||
|
"glib",
|
||||||
|
"graphene-rs",
|
||||||
|
"gsk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gsk4-sys"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23024bf2636c38bbd1f822f58acc9d1c25b28da896ff0f291a1a232d4272b3dc"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"gdk4-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"graphene-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b04e11319b08af11358ab543105a9e49b0c491faca35e2b8e7e36bfba8b671ab"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"field-offset",
|
||||||
|
"futures-channel",
|
||||||
|
"gdk-pixbuf",
|
||||||
|
"gdk4",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"graphene-rs",
|
||||||
|
"gsk4",
|
||||||
|
"gtk4-macros",
|
||||||
|
"gtk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4-macros"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec655a7ef88d8ce9592899deb8b2d0fa50bab1e6dd69182deb764e643c522408"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gtk4-sys"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c8aa86b7f85ea71d66ea88c1d4bae1cfacf51ca4856274565133838d77e57b5"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"gdk-pixbuf-sys",
|
||||||
|
"gdk4-sys",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"graphene-sys",
|
||||||
|
"gsk4-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.182"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nesemu"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nesemu-adapter-api",
|
||||||
|
"nesemu-adapter-headless",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nesemu-adapter-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nesemu-adapter-headless"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"nesemu-adapter-api",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nesemu-desktop"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"gtk4",
|
||||||
|
"nesemu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pango"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f0d328648058085cfd6897c9ae4272884098a926f3a833cd50c8c73e6eccecd"
|
||||||
|
dependencies = [
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pango-sys"
|
||||||
|
version = "0.19.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff03da4fa086c0b244d4a4587d3e20622a3ecdb21daea9edf66597224c634ba0"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit 0.25.4+spec-1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-deps"
|
||||||
|
version = "6.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-expr",
|
||||||
|
"heck",
|
||||||
|
"pkg-config",
|
||||||
|
"toml",
|
||||||
|
"version-compare",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "target-lexicon"
|
||||||
|
version = "0.12.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_edit 0.22.27",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.0.0+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.4+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime 1.0.0+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.0.9+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version-compare"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
42
Cargo.toml
Normal file
42
Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
[package]
|
||||||
|
name = "nesemu"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Core NES/Famicom emulation library in Rust"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
documentation = "https://docs.rs/nesemu"
|
||||||
|
keywords = ["nes", "emulator", "famicom", "gamedev"]
|
||||||
|
categories = ["emulators", "games"]
|
||||||
|
rust-version = "1.85"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
".",
|
||||||
|
"crates/nesemu-adapter-api",
|
||||||
|
"crates/nesemu-adapter-headless",
|
||||||
|
"crates/nesemu-desktop",
|
||||||
|
]
|
||||||
|
default-members = ["."]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nesemu-adapter-api = { path = "crates/nesemu-adapter-api", optional = true }
|
||||||
|
nesemu-adapter-headless = { path = "crates/nesemu-adapter-headless", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
adapter-api = ["dep:nesemu-adapter-api"]
|
||||||
|
adapter-headless = ["adapter-api", "dep:nesemu-adapter-headless"]
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
unreachable_pub = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
pedantic = { level = "allow", priority = -1 }
|
||||||
|
wildcard_imports = "allow"
|
||||||
|
match_same_arms = "allow"
|
||||||
|
module_name_repetitions = "allow"
|
||||||
|
too_many_lines = "allow"
|
||||||
|
needless_pass_by_value = "allow"
|
||||||
102
README.md
Normal file
102
README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# nesemu
|
||||||
|
|
||||||
|
NES/Famicom emulation workspace in Rust.
|
||||||
|
|
||||||
|
The workspace is built around a reusable core library. It also contains optional adapter crates and a GTK4 desktop frontend for manual testing.
|
||||||
|
|
||||||
|
## What Is Here
|
||||||
|
|
||||||
|
- `nesemu`: core emulation library
|
||||||
|
- `nesemu-adapter-api`: backend-agnostic adapter traits
|
||||||
|
- `nesemu-adapter-headless`: null/headless adapter implementations
|
||||||
|
- `nesemu-desktop`: GTK4 desktop frontend
|
||||||
|
|
||||||
|
## What The Core Library Provides
|
||||||
|
|
||||||
|
- CPU, PPU, APU, bus, and cartridge mapper emulation
|
||||||
|
- iNES ROM parsing
|
||||||
|
- Save/load state support
|
||||||
|
- Host-facing runtime wrappers for frame execution and pacing
|
||||||
|
- Public API and behavior tests
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Add the main crate as a dependency:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
nesemu = { path = "../nesemu" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable optional adapter support if needed:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
nesemu = { path = "../nesemu", features = ["adapter-api", "adapter-headless"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended import style:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use nesemu::prelude::*;
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimal setup:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use nesemu::{Cpu6502, NativeBus, create_mapper, parse_rom};
|
||||||
|
|
||||||
|
let rom_bytes = std::fs::read("game.nes")?;
|
||||||
|
let rom = parse_rom(&rom_bytes)?;
|
||||||
|
let mapper = create_mapper(rom)?;
|
||||||
|
let mut bus = NativeBus::new(mapper);
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
```
|
||||||
|
|
||||||
|
Higher-level runtime setup:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use nesemu::{FRAME_RGBA_BYTES, NesRuntime};
|
||||||
|
|
||||||
|
let rom_bytes = std::fs::read("game.nes")?;
|
||||||
|
let mut runtime = NesRuntime::from_rom_bytes(&rom_bytes)?;
|
||||||
|
runtime.run_until_frame_complete()?;
|
||||||
|
|
||||||
|
let mut rgba = vec![0; FRAME_RGBA_BYTES];
|
||||||
|
runtime.render_frame_rgba(&mut rgba)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desktop Frontend
|
||||||
|
|
||||||
|
Run the GTK4 desktop frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p nesemu-desktop -- path/to/game.nes
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux build requirements: GTK4 development packages and `pkg-config` (for example on Debian/Ubuntu: `libgtk-4-dev pkg-config`).
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
- `Esc`: quit
|
||||||
|
- `P`: pause/resume
|
||||||
|
- `Open ROM`: load `.nes` file
|
||||||
|
- Arrow keys: D-pad
|
||||||
|
- `X`: A
|
||||||
|
- `Z`: B
|
||||||
|
- `Enter`: Start
|
||||||
|
- `Left Shift` / `Right Shift`: Select
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
cargo test --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Map
|
||||||
|
|
||||||
|
- [API Contract](docs/api_contract.md): supported external surface and stability expectations
|
||||||
|
- [Integration Guide](docs/integration.md): how to embed the library into a host or frontend
|
||||||
|
- [Architecture](docs/architecture.md): internal module layout and layering
|
||||||
9
crates/nesemu-adapter-api/Cargo.toml
Normal file
9
crates/nesemu-adapter-api/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "nesemu-adapter-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Backend-agnostic adapter traits for nesemu clients"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
rust-version = "1.85"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
70
crates/nesemu-adapter-api/src/lib.rs
Normal file
70
crates/nesemu-adapter-api/src/lib.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub const BUTTONS_COUNT: usize = 8;
|
||||||
|
pub type ButtonState = [bool; BUTTONS_COUNT];
|
||||||
|
|
||||||
|
pub trait InputSource {
|
||||||
|
fn poll_buttons(&mut self) -> ButtonState;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait VideoSink {
|
||||||
|
fn present_rgba(&mut self, frame: &[u8], width: u32, height: u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AudioSink {
|
||||||
|
fn push_samples(&mut self, samples: &[f32]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TimeSource {
|
||||||
|
fn wait_next_frame(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum StorageError {
|
||||||
|
NotFound(String),
|
||||||
|
Io(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FileStore {
|
||||||
|
fn read(&self, key: &str) -> Result<Vec<u8>, StorageError>;
|
||||||
|
fn write(&mut self, key: &str, bytes: &[u8]) -> Result<(), StorageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MemoryStore {
|
||||||
|
items: HashMap<String, Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileStore for MemoryStore {
|
||||||
|
fn read(&self, key: &str) -> Result<Vec<u8>, StorageError> {
|
||||||
|
self.items
|
||||||
|
.get(key)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| StorageError::NotFound(key.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, key: &str, bytes: &[u8]) -> Result<(), StorageError> {
|
||||||
|
self.items.insert(key.to_string(), bytes.to_vec());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{FileStore, MemoryStore, StorageError};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memory_store_roundtrip() {
|
||||||
|
let mut store = MemoryStore::default();
|
||||||
|
store.write("slot1", &[1, 2, 3]).expect("write");
|
||||||
|
assert_eq!(store.read("slot1").expect("read"), vec![1, 2, 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memory_store_not_found() {
|
||||||
|
let store = MemoryStore::default();
|
||||||
|
let err = store.read("missing").expect_err("must fail");
|
||||||
|
assert!(matches!(err, StorageError::NotFound(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
10
crates/nesemu-adapter-headless/Cargo.toml
Normal file
10
crates/nesemu-adapter-headless/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "nesemu-adapter-headless"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Headless/null adapter implementations for nesemu adapter API"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
rust-version = "1.85"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nesemu-adapter-api = { path = "../nesemu-adapter-api" }
|
||||||
52
crates/nesemu-adapter-headless/src/lib.rs
Normal file
52
crates/nesemu-adapter-headless/src/lib.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use nesemu_adapter_api::{
|
||||||
|
AudioSink, BUTTONS_COUNT, ButtonState, InputSource, TimeSource, VideoSink,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NullInput;
|
||||||
|
|
||||||
|
impl InputSource for NullInput {
|
||||||
|
fn poll_buttons(&mut self) -> ButtonState {
|
||||||
|
[false; BUTTONS_COUNT]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NullVideo;
|
||||||
|
|
||||||
|
impl VideoSink for NullVideo {
|
||||||
|
fn present_rgba(&mut self, _frame: &[u8], _width: u32, _height: u32) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NullAudio;
|
||||||
|
|
||||||
|
impl AudioSink for NullAudio {
|
||||||
|
fn push_samples(&mut self, _samples: &[f32]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NoopTime;
|
||||||
|
|
||||||
|
impl TimeSource for NoopTime {
|
||||||
|
fn wait_next_frame(&mut self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{NoopTime, NullAudio, NullInput, NullVideo};
|
||||||
|
use nesemu_adapter_api::{AudioSink, BUTTONS_COUNT, InputSource, TimeSource, VideoSink};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn null_adapters_are_noop() {
|
||||||
|
let mut input = NullInput;
|
||||||
|
let mut video = NullVideo;
|
||||||
|
let mut audio = NullAudio;
|
||||||
|
let mut time = NoopTime;
|
||||||
|
|
||||||
|
assert_eq!(input.poll_buttons(), [false; BUTTONS_COUNT]);
|
||||||
|
video.present_rgba(&[], 256, 240);
|
||||||
|
audio.push_samples(&[]);
|
||||||
|
time.wait_next_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/nesemu-desktop/Cargo.toml
Normal file
9
crates/nesemu-desktop/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "nesemu-desktop"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nesemu = { path = "../.." }
|
||||||
|
gtk4 = "0.8"
|
||||||
|
cairo-rs = "0.19"
|
||||||
505
crates/nesemu-desktop/src/main.rs
Normal file
505
crates/nesemu-desktop/src/main.rs
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gtk::gio;
|
||||||
|
use gtk::gdk;
|
||||||
|
use gtk::glib;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk4 as gtk;
|
||||||
|
use nesemu::prelude::{EmulationState, HostConfig, RuntimeHostLoop};
|
||||||
|
use nesemu::{
|
||||||
|
FRAME_HEIGHT, FRAME_RGBA_BYTES, FRAME_WIDTH, FrameClock, InputProvider, JoypadButton,
|
||||||
|
JoypadButtons, NesRuntime, set_button_pressed,
|
||||||
|
};
|
||||||
|
|
||||||
|
const APP_ID: &str = "org.nesemu.desktop";
|
||||||
|
const TITLE: &str = "NES Emulator";
|
||||||
|
const SCALE: i32 = 3;
|
||||||
|
const SAMPLE_RATE: u32 = 48_000;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if std::env::var_os("GSK_RENDERER").is_none() {
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("GSK_RENDERER", "cairo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = gtk::Application::builder()
|
||||||
|
.application_id(APP_ID)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let initial_rom: Rc<RefCell<Option<PathBuf>>> =
|
||||||
|
Rc::new(RefCell::new(std::env::args().nth(1).map(PathBuf::from)));
|
||||||
|
|
||||||
|
let initial_rom_for_activate = Rc::clone(&initial_rom);
|
||||||
|
app.connect_activate(move |app| {
|
||||||
|
let rom = initial_rom_for_activate.borrow_mut().take();
|
||||||
|
build_ui(app, rom);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run_with_args::<&str>(&[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ui(app: >k::Application, initial_rom: Option<PathBuf>) {
|
||||||
|
let window = gtk::ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.title(TITLE)
|
||||||
|
.default_width((FRAME_WIDTH as i32) * SCALE)
|
||||||
|
.default_height((FRAME_HEIGHT as i32) * SCALE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// --- Header bar ---
|
||||||
|
let header = gtk::HeaderBar::new();
|
||||||
|
|
||||||
|
let open_button = gtk::Button::builder()
|
||||||
|
.icon_name("document-open-symbolic")
|
||||||
|
.tooltip_text("Open ROM (Ctrl+O)")
|
||||||
|
.focusable(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let pause_button = gtk::Button::builder()
|
||||||
|
.icon_name("media-playback-pause-symbolic")
|
||||||
|
.tooltip_text("Pause / Resume (P)")
|
||||||
|
.focusable(false)
|
||||||
|
.sensitive(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let reset_button = gtk::Button::builder()
|
||||||
|
.icon_name("view-refresh-symbolic")
|
||||||
|
.tooltip_text("Reset (Ctrl+R)")
|
||||||
|
.focusable(false)
|
||||||
|
.sensitive(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
header.pack_start(&open_button);
|
||||||
|
header.pack_start(&pause_button);
|
||||||
|
header.pack_start(&reset_button);
|
||||||
|
|
||||||
|
window.set_titlebar(Some(&header));
|
||||||
|
|
||||||
|
// --- Drawing area ---
|
||||||
|
let drawing_area = gtk::DrawingArea::new();
|
||||||
|
drawing_area.set_hexpand(true);
|
||||||
|
drawing_area.set_vexpand(true);
|
||||||
|
|
||||||
|
let overlay = gtk::Overlay::new();
|
||||||
|
overlay.set_child(Some(&drawing_area));
|
||||||
|
|
||||||
|
let drop_label = gtk::Label::builder()
|
||||||
|
.label("Drop a .nes ROM here\nor press Ctrl+O to open")
|
||||||
|
.justify(gtk::Justification::Center)
|
||||||
|
.css_classes(["dim-label"])
|
||||||
|
.build();
|
||||||
|
drop_label.set_halign(gtk::Align::Center);
|
||||||
|
drop_label.set_valign(gtk::Align::Center);
|
||||||
|
overlay.add_overlay(&drop_label);
|
||||||
|
|
||||||
|
window.set_child(Some(&overlay));
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
let desktop = Rc::new(RefCell::new(DesktopApp::new()));
|
||||||
|
let frame_for_draw: Rc<RefCell<Vec<u8>>> =
|
||||||
|
Rc::new(RefCell::new(vec![0u8; FRAME_RGBA_BYTES]));
|
||||||
|
|
||||||
|
// --- Draw function (pixel-perfect nearest-neighbor) ---
|
||||||
|
{
|
||||||
|
let frame_for_draw = Rc::clone(&frame_for_draw);
|
||||||
|
drawing_area.set_draw_func(move |_da, cr, width, height| {
|
||||||
|
let frame = frame_for_draw.borrow();
|
||||||
|
let stride =
|
||||||
|
cairo::Format::ARgb32.stride_for_width(FRAME_WIDTH as u32).unwrap();
|
||||||
|
let mut argb = vec![0u8; stride as usize * FRAME_HEIGHT];
|
||||||
|
for y in 0..FRAME_HEIGHT {
|
||||||
|
for x in 0..FRAME_WIDTH {
|
||||||
|
let src = (y * FRAME_WIDTH + x) * 4;
|
||||||
|
let dst = y * stride as usize + x * 4;
|
||||||
|
let r = frame[src];
|
||||||
|
let g = frame[src + 1];
|
||||||
|
let b = frame[src + 2];
|
||||||
|
let a = frame[src + 3];
|
||||||
|
argb[dst] = b;
|
||||||
|
argb[dst + 1] = g;
|
||||||
|
argb[dst + 2] = r;
|
||||||
|
argb[dst + 3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let surface = cairo::ImageSurface::create_for_data(
|
||||||
|
argb,
|
||||||
|
cairo::Format::ARgb32,
|
||||||
|
FRAME_WIDTH as i32,
|
||||||
|
FRAME_HEIGHT as i32,
|
||||||
|
stride,
|
||||||
|
)
|
||||||
|
.expect("Failed to create Cairo surface");
|
||||||
|
|
||||||
|
// Fill background black
|
||||||
|
let _ = cr.set_source_rgb(0.0, 0.0, 0.0);
|
||||||
|
let _ = cr.paint();
|
||||||
|
|
||||||
|
let sx = width as f64 / FRAME_WIDTH as f64;
|
||||||
|
let sy = height as f64 / FRAME_HEIGHT as f64;
|
||||||
|
let scale = sx.min(sy);
|
||||||
|
let offset_x = (width as f64 - FRAME_WIDTH as f64 * scale) / 2.0;
|
||||||
|
let offset_y = (height as f64 - FRAME_HEIGHT as f64 * scale) / 2.0;
|
||||||
|
|
||||||
|
let _ = cr.translate(offset_x, offset_y);
|
||||||
|
let _ = cr.scale(scale, scale);
|
||||||
|
let _ = cr.set_source_surface(&surface, 0.0, 0.0);
|
||||||
|
cr.source().set_filter(cairo::Filter::Nearest);
|
||||||
|
let _ = cr.paint();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper to sync UI with emulation state ---
|
||||||
|
let sync_ui = {
|
||||||
|
let pause_button = pause_button.clone();
|
||||||
|
let reset_button = reset_button.clone();
|
||||||
|
let drop_label = drop_label.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
move |app_state: &DesktopApp, rom_name: Option<&str>| {
|
||||||
|
let loaded = app_state.is_loaded();
|
||||||
|
pause_button.set_sensitive(loaded);
|
||||||
|
reset_button.set_sensitive(loaded);
|
||||||
|
drop_label.set_visible(!loaded);
|
||||||
|
|
||||||
|
if app_state.state() == EmulationState::Running {
|
||||||
|
pause_button.set_icon_name("media-playback-pause-symbolic");
|
||||||
|
pause_button.set_tooltip_text(Some("Pause (P)"));
|
||||||
|
} else {
|
||||||
|
pause_button.set_icon_name("media-playback-start-symbolic");
|
||||||
|
pause_button.set_tooltip_text(Some("Resume (P)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name) = rom_name {
|
||||||
|
window.set_title(Some(&format!("{TITLE} — {name}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let sync_ui = Rc::new(sync_ui);
|
||||||
|
|
||||||
|
// --- Load initial ROM ---
|
||||||
|
{
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
if let Some(path) = initial_rom {
|
||||||
|
if let Err(err) = app_state.load_rom_from_path(&path) {
|
||||||
|
eprintln!("Failed to load ROM '{}': {err}", path.display());
|
||||||
|
sync_ui(&app_state, None);
|
||||||
|
} else {
|
||||||
|
let name = rom_filename(&path);
|
||||||
|
sync_ui(&app_state, Some(&name));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sync_ui(&app_state, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Open ROM handler ---
|
||||||
|
let do_open_rom = {
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let sync_ui = Rc::clone(&sync_ui);
|
||||||
|
let window = window.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
let chooser = gtk::FileChooserNative::new(
|
||||||
|
Some("Open NES ROM"),
|
||||||
|
Some(&window),
|
||||||
|
gtk::FileChooserAction::Open,
|
||||||
|
Some("Open"),
|
||||||
|
Some("Cancel"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let nes_filter = gtk::FileFilter::new();
|
||||||
|
nes_filter.set_name(Some("NES ROMs"));
|
||||||
|
nes_filter.add_pattern("*.nes");
|
||||||
|
chooser.add_filter(&nes_filter);
|
||||||
|
let all_filter = gtk::FileFilter::new();
|
||||||
|
all_filter.set_name(Some("All files"));
|
||||||
|
all_filter.add_pattern("*");
|
||||||
|
chooser.add_filter(&all_filter);
|
||||||
|
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let sync_ui = Rc::clone(&sync_ui);
|
||||||
|
chooser.connect_response(move |dialog, response| {
|
||||||
|
if response == gtk::ResponseType::Accept {
|
||||||
|
if let Some(path) = dialog.file().and_then(|f| f.path()) {
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
if let Err(err) = app_state.load_rom_from_path(&path) {
|
||||||
|
eprintln!("Failed to load ROM '{}': {err}", path.display());
|
||||||
|
} else {
|
||||||
|
let name = rom_filename(&path);
|
||||||
|
sync_ui(&app_state, Some(&name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chooser.show();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Button handlers ---
|
||||||
|
{
|
||||||
|
let do_open_rom = Rc::clone(&do_open_rom);
|
||||||
|
open_button.connect_clicked(move |_| {
|
||||||
|
do_open_rom();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let sync_ui = Rc::clone(&sync_ui);
|
||||||
|
pause_button.connect_clicked(move |_| {
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
app_state.toggle_pause();
|
||||||
|
sync_ui(&app_state, None);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let sync_ui = Rc::clone(&sync_ui);
|
||||||
|
reset_button.connect_clicked(move |_| {
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
app_state.reset();
|
||||||
|
sync_ui(&app_state, None);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Keyboard shortcuts via actions ---
|
||||||
|
let action_open = gio::SimpleAction::new("open", None);
|
||||||
|
{
|
||||||
|
let do_open_rom = Rc::clone(&do_open_rom);
|
||||||
|
action_open.connect_activate(move |_, _| {
|
||||||
|
do_open_rom();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.add_action(&action_open);
|
||||||
|
app.set_accels_for_action("win.open", &["<Ctrl>o"]);
|
||||||
|
|
||||||
|
let action_pause = gio::SimpleAction::new("toggle-pause", None);
|
||||||
|
{
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let sync_ui = Rc::clone(&sync_ui);
|
||||||
|
action_pause.connect_activate(move |_, _| {
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
if app_state.is_loaded() {
|
||||||
|
app_state.toggle_pause();
|
||||||
|
sync_ui(&app_state, None);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.add_action(&action_pause);
|
||||||
|
app.set_accels_for_action("win.toggle-pause", &["p"]);
|
||||||
|
|
||||||
|
let action_reset = gio::SimpleAction::new("reset", None);
|
||||||
|
{
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let sync_ui = Rc::clone(&sync_ui);
|
||||||
|
action_reset.connect_activate(move |_, _| {
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
if app_state.is_loaded() {
|
||||||
|
app_state.reset();
|
||||||
|
sync_ui(&app_state, None);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.add_action(&action_reset);
|
||||||
|
app.set_accels_for_action("win.reset", &["<Ctrl>r"]);
|
||||||
|
|
||||||
|
// --- Keyboard controller for joypad input ---
|
||||||
|
{
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let key_controller = gtk::EventControllerKey::new();
|
||||||
|
|
||||||
|
let desktop_for_press = Rc::clone(&desktop);
|
||||||
|
key_controller.connect_key_pressed(move |_, key, _, _| {
|
||||||
|
let mut app_state = desktop_for_press.borrow_mut();
|
||||||
|
app_state.input_mut().set_key_state(key, true);
|
||||||
|
gtk::glib::Propagation::Proceed
|
||||||
|
});
|
||||||
|
|
||||||
|
key_controller.connect_key_released(move |_, key, _, _| {
|
||||||
|
desktop.borrow_mut().input_mut().set_key_state(key, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.add_controller(key_controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Drag-and-drop ---
|
||||||
|
{
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let sync_ui = Rc::clone(&sync_ui);
|
||||||
|
let drop_target = gtk::DropTarget::new(gio::File::static_type(), gdk::DragAction::COPY);
|
||||||
|
drop_target.connect_drop(move |_, value, _, _| {
|
||||||
|
if let Ok(file) = value.get::<gio::File>() {
|
||||||
|
if let Some(path) = file.path() {
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
if let Err(err) = app_state.load_rom_from_path(&path) {
|
||||||
|
eprintln!("Failed to load ROM '{}': {err}", path.display());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let name = rom_filename(&path);
|
||||||
|
sync_ui(&app_state, Some(&name));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
});
|
||||||
|
drawing_area.add_controller(drop_target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Game loop ---
|
||||||
|
{
|
||||||
|
let desktop = Rc::clone(&desktop);
|
||||||
|
let drawing_area = drawing_area.clone();
|
||||||
|
let frame_for_draw = Rc::clone(&frame_for_draw);
|
||||||
|
glib::timeout_add_local(Duration::from_millis(16), move || {
|
||||||
|
let mut app_state = desktop.borrow_mut();
|
||||||
|
app_state.tick();
|
||||||
|
|
||||||
|
frame_for_draw
|
||||||
|
.borrow_mut()
|
||||||
|
.copy_from_slice(app_state.frame_rgba());
|
||||||
|
drawing_area.queue_draw();
|
||||||
|
|
||||||
|
glib::ControlFlow::Continue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rom_filename(path: &Path) -> String {
|
||||||
|
path.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "Unknown".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Input
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct InputState {
|
||||||
|
buttons: JoypadButtons,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
fn set_key_state(&mut self, key: gdk::Key, pressed: bool) {
|
||||||
|
let button = match key {
|
||||||
|
gdk::Key::Up => JoypadButton::Up,
|
||||||
|
gdk::Key::Down => JoypadButton::Down,
|
||||||
|
gdk::Key::Left => JoypadButton::Left,
|
||||||
|
gdk::Key::Right => JoypadButton::Right,
|
||||||
|
gdk::Key::x | gdk::Key::X => JoypadButton::A,
|
||||||
|
gdk::Key::z | gdk::Key::Z => JoypadButton::B,
|
||||||
|
gdk::Key::Return => JoypadButton::Start,
|
||||||
|
gdk::Key::Shift_L | gdk::Key::Shift_R => JoypadButton::Select,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
set_button_pressed(&mut self.buttons, button, pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputProvider for InputState {
|
||||||
|
fn poll_buttons(&mut self) -> JoypadButtons {
|
||||||
|
self.buttons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Audio (stub)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct AudioSink;
|
||||||
|
|
||||||
|
impl nesemu::AudioOutput for AudioSink {
|
||||||
|
fn push_samples(&mut self, _samples: &[f32]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Application state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct DesktopApp {
|
||||||
|
host: Option<RuntimeHostLoop<Box<dyn FrameClock>>>,
|
||||||
|
input: InputState,
|
||||||
|
audio: AudioSink,
|
||||||
|
frame_rgba: Vec<u8>,
|
||||||
|
state: EmulationState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopApp {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
host: None,
|
||||||
|
input: InputState::default(),
|
||||||
|
audio: AudioSink,
|
||||||
|
frame_rgba: vec![0; FRAME_RGBA_BYTES],
|
||||||
|
state: EmulationState::Paused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_rom_from_path(&mut self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let data = std::fs::read(path)?;
|
||||||
|
let runtime = NesRuntime::from_rom_bytes(&data)?;
|
||||||
|
let config = HostConfig::new(SAMPLE_RATE, false);
|
||||||
|
self.host = Some(RuntimeHostLoop::with_config(runtime, config));
|
||||||
|
self.state = EmulationState::Running;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
if let Some(host) = self.host.as_mut() {
|
||||||
|
host.runtime_mut().reset();
|
||||||
|
self.state = EmulationState::Running;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_loaded(&self) -> bool {
|
||||||
|
self.host.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> EmulationState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_pause(&mut self) {
|
||||||
|
self.state = match self.state {
|
||||||
|
EmulationState::Running => EmulationState::Paused,
|
||||||
|
EmulationState::Paused => EmulationState::Running,
|
||||||
|
_ => EmulationState::Paused,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&mut self) {
|
||||||
|
if self.state != EmulationState::Running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(host) = self.host.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut null_video = nesemu::NullVideo;
|
||||||
|
if let Err(err) = host.run_frame_unpaced(&mut self.input, &mut null_video, &mut self.audio)
|
||||||
|
{
|
||||||
|
eprintln!("Frame execution error: {err}");
|
||||||
|
self.state = EmulationState::Paused;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.frame_rgba
|
||||||
|
.copy_from_slice(&host.runtime().frame_rgba());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_rgba(&self) -> &[u8] {
|
||||||
|
&self.frame_rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_mut(&mut self) -> &mut InputState {
|
||||||
|
&mut self.input
|
||||||
|
}
|
||||||
|
}
|
||||||
116
docs/api_contract.md
Normal file
116
docs/api_contract.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# API Contract
|
||||||
|
|
||||||
|
This document defines the supported external contract for `nesemu` `0.x`.
|
||||||
|
|
||||||
|
Use this file as the boundary of what external clients should rely on. For practical embedding examples, see `integration.md`. For internal structure, see `architecture.md`.
|
||||||
|
|
||||||
|
## Supported Surface
|
||||||
|
|
||||||
|
External users should prefer these entry points:
|
||||||
|
|
||||||
|
- Root crate re-exports from `src/lib.rs`
|
||||||
|
- `nesemu::runtime::*`
|
||||||
|
- `nesemu::prelude::*`
|
||||||
|
|
||||||
|
Optional adapter-facing API is available behind features:
|
||||||
|
|
||||||
|
- `adapter-api`
|
||||||
|
- `adapter-headless`
|
||||||
|
|
||||||
|
## Recommended Public Entry Points
|
||||||
|
|
||||||
|
The main public API is organized around these groups:
|
||||||
|
|
||||||
|
- ROM loading:
|
||||||
|
- `parse_header`
|
||||||
|
- `parse_rom`
|
||||||
|
- `InesHeader`
|
||||||
|
- `InesRom`
|
||||||
|
- `Mirroring`
|
||||||
|
- Cartridge mapping:
|
||||||
|
- `create_mapper`
|
||||||
|
- `Mapper`
|
||||||
|
- Low-level execution:
|
||||||
|
- `Cpu6502`
|
||||||
|
- `CpuBus`
|
||||||
|
- `CpuError`
|
||||||
|
- `NativeBus`
|
||||||
|
- High-level runtime:
|
||||||
|
- `NesRuntime`
|
||||||
|
- Host execution and lifecycle:
|
||||||
|
- `RuntimeHostLoop`
|
||||||
|
- `ClientRuntime`
|
||||||
|
- `HostConfig`
|
||||||
|
- `EmulationState`
|
||||||
|
- Host IO traits:
|
||||||
|
- `InputProvider`
|
||||||
|
- `VideoOutput`
|
||||||
|
- `AudioOutput`
|
||||||
|
- Timing and pacing:
|
||||||
|
- `FrameClock`
|
||||||
|
- `FramePacer`
|
||||||
|
- `PacingClock`
|
||||||
|
- `NoopClock`
|
||||||
|
- `VideoMode`
|
||||||
|
- Input helpers:
|
||||||
|
- `JoypadButton`
|
||||||
|
- `JoypadButtons`
|
||||||
|
- `JOYPAD_BUTTON_ORDER`
|
||||||
|
- `JOYPAD_BUTTONS_COUNT`
|
||||||
|
- `set_button_pressed`
|
||||||
|
- `button_pressed`
|
||||||
|
|
||||||
|
## Supported Client Flow
|
||||||
|
|
||||||
|
The expected integration flow is:
|
||||||
|
|
||||||
|
1. Load ROM bytes and parse them, or construct `NesRuntime` directly from ROM bytes.
|
||||||
|
2. Choose your integration level:
|
||||||
|
- use `Cpu6502` + `NativeBus` for low-level control
|
||||||
|
- use `NesRuntime` for a higher-level core wrapper
|
||||||
|
- use `RuntimeHostLoop` or `ClientRuntime` for host-facing frame execution
|
||||||
|
3. Provide input, video, and audio implementations through the public host traits.
|
||||||
|
4. Use save/load state through the runtime or bus APIs when snapshot behavior is needed.
|
||||||
|
|
||||||
|
## Stability Rules
|
||||||
|
|
||||||
|
The following are considered the primary supported surface for `0.x`:
|
||||||
|
|
||||||
|
- root re-exports
|
||||||
|
- `runtime`
|
||||||
|
- `prelude`
|
||||||
|
|
||||||
|
The following are available but less stable:
|
||||||
|
|
||||||
|
- `native_core::*` for advanced or experimental integrations
|
||||||
|
|
||||||
|
Lower-level modules may evolve faster than the root re-export surface.
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
- Types marked `#[non_exhaustive]` may gain fields or variants without a major version bump.
|
||||||
|
- Save-state compatibility is only guaranteed within the same crate version unless explicitly documented otherwise.
|
||||||
|
- Optional features may expose additional adapter-facing API, but they do not change the baseline contract of the main library.
|
||||||
|
|
||||||
|
## Extension Points
|
||||||
|
|
||||||
|
The intended extension points for hosts and frontends are:
|
||||||
|
|
||||||
|
- `InputProvider`
|
||||||
|
- `VideoOutput`
|
||||||
|
- `AudioOutput`
|
||||||
|
- `FrameClock`
|
||||||
|
- optional adapter bridge types when `adapter-api` is enabled:
|
||||||
|
- `InputAdapter`
|
||||||
|
- `VideoAdapter`
|
||||||
|
- `AudioAdapter`
|
||||||
|
- `ClockAdapter`
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
This contract does not promise stability for:
|
||||||
|
|
||||||
|
- GTK frontend behavior in `nesemu-desktop`
|
||||||
|
- internal module layout under `native_core` and `runtime`
|
||||||
|
- concrete implementation details of mapper modules
|
||||||
|
- cross-version save-state compatibility unless explicitly documented
|
||||||
107
docs/architecture.md
Normal file
107
docs/architecture.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
This document describes how the workspace is organized internally.
|
||||||
|
|
||||||
|
Use `../README.md` for project overview, `integration.md` for host integration, and `api_contract.md` for supported public surface.
|
||||||
|
|
||||||
|
## Workspace Layout
|
||||||
|
|
||||||
|
- `nesemu`: reusable emulation library and host-facing runtime wrappers
|
||||||
|
- `crates/nesemu-adapter-api`: backend-agnostic adapter traits
|
||||||
|
- `crates/nesemu-adapter-headless`: headless/null adapter implementations
|
||||||
|
- `crates/nesemu-desktop`: GTK4 desktop frontend that consumes the root crate
|
||||||
|
|
||||||
|
## High-Level Layers
|
||||||
|
|
||||||
|
The workspace is split into four layers:
|
||||||
|
|
||||||
|
1. `native_core`
|
||||||
|
Owns emulation correctness and hardware-facing behavior.
|
||||||
|
2. `runtime`
|
||||||
|
Wraps the core with host-oriented execution, pacing, lifecycle control, and save-state helpers.
|
||||||
|
3. adapter crates
|
||||||
|
Define integration edges without coupling the core to a concrete backend.
|
||||||
|
4. desktop frontend
|
||||||
|
Serves as a consumer and manual test harness, not as part of the library contract.
|
||||||
|
|
||||||
|
## Core Module Boundaries
|
||||||
|
|
||||||
|
- `src/native_core/cpu`: 6502 execution, addressing helpers, opcode dispatch
|
||||||
|
- `src/native_core/ppu`: rendering pipeline, VRAM/OAM/register behavior
|
||||||
|
- `src/native_core/apu`: timing, channel state, and audio-facing hardware state
|
||||||
|
- `src/native_core/bus`: component wiring and device-visible read/write semantics
|
||||||
|
- `src/native_core/mapper`: cartridge mapper abstraction and concrete implementations
|
||||||
|
- `src/native_core/ines`: iNES parsing and ROM metadata
|
||||||
|
- `src/native_core/state_io`: shared state decoding helpers
|
||||||
|
|
||||||
|
## Runtime Module Boundaries
|
||||||
|
|
||||||
|
- `src/runtime/core.rs`: `NesRuntime` orchestration around CPU + bus
|
||||||
|
- `src/runtime/state.rs`: runtime save/load state format
|
||||||
|
- `src/runtime/audio.rs`: interim PCM synthesis from core state
|
||||||
|
- `src/runtime/timing.rs`: frame pacing types and video timing
|
||||||
|
- `src/runtime/types.rs`: public joypad-related types and helpers
|
||||||
|
- `src/runtime/host/io.rs`: host IO traits and null implementations
|
||||||
|
- `src/runtime/host/executor.rs`: per-frame execution unit
|
||||||
|
- `src/runtime/host/clock.rs`: clock abstraction and pacing implementations
|
||||||
|
- `src/runtime/host/loop_runner.rs`: host loop wrapper for frame-based execution
|
||||||
|
- `src/runtime/host/session.rs`: app lifecycle wrapper for running, pausing, and stepping
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
At a high level, the runtime stack looks like this:
|
||||||
|
|
||||||
|
1. ROM bytes are parsed into cartridge metadata and ROM contents.
|
||||||
|
2. A mapper is created from the ROM description.
|
||||||
|
3. `NativeBus` wires CPU, PPU, APU, mapper, and input-visible state together.
|
||||||
|
4. `Cpu6502` executes against the bus.
|
||||||
|
5. `NesRuntime` wraps the core to provide frame-level execution, rendering, and save-state helpers.
|
||||||
|
6. `RuntimeHostLoop` and `ClientRuntime` adapt the runtime to host application control flow.
|
||||||
|
|
||||||
|
## Public Surface Strategy
|
||||||
|
|
||||||
|
The root crate re-exports the integration-critical API so external users do not need to depend on the internal module layout.
|
||||||
|
|
||||||
|
The design intent is:
|
||||||
|
|
||||||
|
- external clients use root re-exports and `runtime`
|
||||||
|
- advanced clients may use `native_core`
|
||||||
|
- internal module paths are free to evolve faster than the root surface
|
||||||
|
|
||||||
|
## Host Responsibilities
|
||||||
|
|
||||||
|
The library intentionally leaves these concerns to the host application:
|
||||||
|
|
||||||
|
- windowing and presentation backend
|
||||||
|
- audio device/output backend
|
||||||
|
- platform input mapping
|
||||||
|
- ROM file I/O
|
||||||
|
- persistent state storage
|
||||||
|
|
||||||
|
## Input, Video, and Audio Contracts
|
||||||
|
|
||||||
|
- `JoypadButtons` are exposed in the public order `[Up, Down, Left, Right, A, B, Start, Select]`
|
||||||
|
- `InputProvider` polls the current button state from the host
|
||||||
|
- `VideoOutput` receives RGBA frames
|
||||||
|
- `AudioOutput` receives mixed mono samples
|
||||||
|
- port 2 is currently treated as disconnected in the exposed core API
|
||||||
|
|
||||||
|
## Save-State Design
|
||||||
|
|
||||||
|
- `NativeBus::save_state` and `NativeBus::load_state` persist low-level emulator state
|
||||||
|
- `NesRuntime` extends that state with runtime metadata such as frame number and active buttons
|
||||||
|
- save-state payloads are versioned for crate-internal use, not for long-term external compatibility
|
||||||
|
|
||||||
|
## Testing Layout
|
||||||
|
|
||||||
|
- CPU tests are grouped by behavior, interrupts, and invariants
|
||||||
|
- mapper tests are grouped by mapper family and property-style checks
|
||||||
|
- runtime tests cover frame execution, pacing, state roundtrips, and lifecycle control
|
||||||
|
- `tests/public_api.rs` exercises the supported public flow as a black-box consumer
|
||||||
|
|
||||||
|
## Constraints And Tradeoffs
|
||||||
|
|
||||||
|
- no platform backend is bundled beyond the GTK desktop example
|
||||||
|
- audio mixing in `runtime/audio.rs` is intentionally interim
|
||||||
|
- optional adapter crates are thin integration layers, not mandatory parts of the core runtime
|
||||||
|
- compatibility promises are defined in `docs/api_contract.md`, not by internal module visibility
|
||||||
177
docs/integration.md
Normal file
177
docs/integration.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Integration Guide
|
||||||
|
|
||||||
|
This guide shows how to embed `nesemu` into a host application or frontend.
|
||||||
|
|
||||||
|
For the stable API boundary, see `api_contract.md`. For internal structure, see `architecture.md`.
|
||||||
|
|
||||||
|
## Choose An Integration Level
|
||||||
|
|
||||||
|
Use the lowest level that matches your needs:
|
||||||
|
|
||||||
|
- `Cpu6502` + `NativeBus`
|
||||||
|
Use this if you need fine-grained stepping or low-level control.
|
||||||
|
- `NesRuntime`
|
||||||
|
Use this if you want frame-oriented execution, rendering helpers, and runtime state handling.
|
||||||
|
- `RuntimeHostLoop`
|
||||||
|
Use this if your host runs the emulator frame-by-frame and wants explicit input, video, audio, and pacing control.
|
||||||
|
- `ClientRuntime`
|
||||||
|
Use this if your app has running/paused/step states and needs lifecycle-oriented ticking.
|
||||||
|
|
||||||
|
## Minimal ROM Load
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use nesemu::{create_mapper, parse_rom, Cpu6502, NativeBus};
|
||||||
|
|
||||||
|
let rom_bytes = std::fs::read("game.nes")?;
|
||||||
|
let rom = parse_rom(&rom_bytes)?;
|
||||||
|
let mapper = create_mapper(rom)?;
|
||||||
|
let mut bus = NativeBus::new(mapper);
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using `NesRuntime`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use nesemu::{FRAME_RGBA_BYTES, NesRuntime};
|
||||||
|
|
||||||
|
let rom_bytes = std::fs::read("game.nes")?;
|
||||||
|
let mut runtime = NesRuntime::from_rom_bytes(&rom_bytes)?;
|
||||||
|
runtime.run_until_frame_complete()?;
|
||||||
|
|
||||||
|
let mut frame = vec![0; FRAME_RGBA_BYTES];
|
||||||
|
runtime.render_frame_rgba(&mut frame)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `NesRuntime` when you want:
|
||||||
|
|
||||||
|
- frame-based stepping instead of raw CPU control
|
||||||
|
- framebuffer extraction
|
||||||
|
- runtime-level save/load state helpers
|
||||||
|
|
||||||
|
## Using `RuntimeHostLoop`
|
||||||
|
|
||||||
|
`RuntimeHostLoop` is the main integration point for hosts that want explicit control over frame execution.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use nesemu::{
|
||||||
|
AudioOutput, HostConfig, InputProvider, JOYPAD_BUTTONS_COUNT, NesRuntime, RuntimeHostLoop,
|
||||||
|
VideoOutput,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Input;
|
||||||
|
impl InputProvider for Input {
|
||||||
|
fn poll_buttons(&mut self) -> [bool; JOYPAD_BUTTONS_COUNT] {
|
||||||
|
[false; JOYPAD_BUTTONS_COUNT]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Video;
|
||||||
|
impl VideoOutput for Video {
|
||||||
|
fn present_rgba(&mut self, _frame: &[u8], _width: usize, _height: usize) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Audio;
|
||||||
|
impl AudioOutput for Audio {
|
||||||
|
fn push_samples(&mut self, _samples: &[f32]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rom_bytes = std::fs::read("game.nes")?;
|
||||||
|
let runtime = NesRuntime::from_rom_bytes(&rom_bytes)?;
|
||||||
|
let mut host = RuntimeHostLoop::with_config(runtime, HostConfig::new(48_000, false));
|
||||||
|
|
||||||
|
let mut input = Input;
|
||||||
|
let mut video = Video;
|
||||||
|
let mut audio = Audio;
|
||||||
|
|
||||||
|
let stats = host.run_frame_unpaced(&mut input, &mut video, &mut audio)?;
|
||||||
|
let _ = stats;
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `run_frame` for paced execution and `run_frame_unpaced` when the host controls timing externally.
|
||||||
|
|
||||||
|
## Using `ClientRuntime`
|
||||||
|
|
||||||
|
`ClientRuntime` wraps the runtime with a simple running/paused/step lifecycle.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use nesemu::{
|
||||||
|
AudioOutput, ClientRuntime, EmulationState, HostConfig, InputProvider, JOYPAD_BUTTONS_COUNT,
|
||||||
|
NesRuntime, VideoOutput,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Input;
|
||||||
|
impl InputProvider for Input {
|
||||||
|
fn poll_buttons(&mut self) -> [bool; JOYPAD_BUTTONS_COUNT] {
|
||||||
|
[false; JOYPAD_BUTTONS_COUNT]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Video;
|
||||||
|
impl VideoOutput for Video {
|
||||||
|
fn present_rgba(&mut self, _frame: &[u8], _width: usize, _height: usize) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Audio;
|
||||||
|
impl AudioOutput for Audio {
|
||||||
|
fn push_samples(&mut self, _samples: &[f32]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rom_bytes = std::fs::read("game.nes")?;
|
||||||
|
let runtime = NesRuntime::from_rom_bytes(&rom_bytes)?;
|
||||||
|
let mut client = ClientRuntime::with_config(runtime, HostConfig::new(48_000, true));
|
||||||
|
|
||||||
|
client.set_state(EmulationState::Running);
|
||||||
|
|
||||||
|
let mut input = Input;
|
||||||
|
let mut video = Video;
|
||||||
|
let mut audio = Audio;
|
||||||
|
let _ = client.tick(&mut input, &mut video, &mut audio)?;
|
||||||
|
|
||||||
|
client.pause();
|
||||||
|
client.step_frame(&mut input, &mut video, &mut audio)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this wrapper when your UI loop naturally switches between running, paused, and manual stepping.
|
||||||
|
|
||||||
|
## Input Mapping
|
||||||
|
|
||||||
|
Public helpers are available to avoid hard-coded button indices:
|
||||||
|
|
||||||
|
- `JoypadButton`
|
||||||
|
- `JOYPAD_BUTTON_ORDER`
|
||||||
|
- `set_button_pressed`
|
||||||
|
- `button_pressed`
|
||||||
|
|
||||||
|
Public button order is:
|
||||||
|
|
||||||
|
`[Up, Down, Left, Right, A, B, Start, Select]`
|
||||||
|
|
||||||
|
## Framebuffer And Audio
|
||||||
|
|
||||||
|
- Video frames are exposed as RGBA8
|
||||||
|
- Frame size is `256x240`
|
||||||
|
- Audio output is a stream of mixed mono `f32` samples
|
||||||
|
- The runtime mixer is usable for host integration, but it is intentionally interim
|
||||||
|
|
||||||
|
## Save-State Use
|
||||||
|
|
||||||
|
Use runtime-level state when you need host-visible frame metadata and input state preserved alongside low-level emulation state.
|
||||||
|
|
||||||
|
Use bus-level state if you are integrating at the low-level core boundary.
|
||||||
|
|
||||||
|
## Optional Adapter Crates
|
||||||
|
|
||||||
|
If you want backend-agnostic adapter traits and headless implementations:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
nesemu = { path = "../nesemu", features = ["adapter-api", "adapter-headless"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(feature = "adapter-api")]
|
||||||
|
use nesemu::adapter_api::{AudioSink, InputSource, TimeSource, VideoSink};
|
||||||
|
```
|
||||||
43
src/lib.rs
Normal file
43
src/lib.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//! `nesemu` is a core NES/Famicom emulation library.
|
||||||
|
//!
|
||||||
|
//! It exposes CPU/PPU/APU/bus primitives and ROM/mapper helpers so a host app
|
||||||
|
//! can provide platform-specific frontend (windowing, audio device, input).
|
||||||
|
//!
|
||||||
|
//! # API Stability
|
||||||
|
//! - `runtime` and root re-exports are the supported public `v0` API for external clients.
|
||||||
|
//! - `native_core` modules are available for advanced integrations, but treated as lower-level
|
||||||
|
//! surface that may evolve faster between minor releases.
|
||||||
|
|
||||||
|
pub mod native_core;
|
||||||
|
pub mod runtime;
|
||||||
|
|
||||||
|
#[cfg(feature = "adapter-api")]
|
||||||
|
pub use nesemu_adapter_api as adapter_api;
|
||||||
|
#[cfg(feature = "adapter-headless")]
|
||||||
|
pub use nesemu_adapter_headless as adapter_headless;
|
||||||
|
|
||||||
|
pub use native_core::apu::{Apu, ApuStateTail};
|
||||||
|
pub use native_core::bus::NativeBus;
|
||||||
|
pub use native_core::cpu::{Cpu6502, CpuBus, CpuError};
|
||||||
|
pub use native_core::ines::{InesHeader, InesRom, Mirroring, parse_header, parse_rom};
|
||||||
|
pub use native_core::mapper::{Mapper, create_mapper};
|
||||||
|
pub use native_core::ppu::Ppu;
|
||||||
|
#[cfg(feature = "adapter-api")]
|
||||||
|
pub use runtime::{AudioAdapter, ClockAdapter, InputAdapter, VideoAdapter};
|
||||||
|
pub use runtime::{
|
||||||
|
AudioMixer, AudioOutput, ClientRuntime, EmulationState, FRAME_HEIGHT, FRAME_RGBA_BYTES,
|
||||||
|
FRAME_WIDTH, FrameClock, FramePacer, HostConfig, InputProvider, JOYPAD_BUTTON_ORDER,
|
||||||
|
JOYPAD_BUTTONS_COUNT, JoypadButton, JoypadButtons, NesRuntime, NoopClock, NullAudio, NullInput,
|
||||||
|
NullVideo, PacingClock, RuntimeError, RuntimeHostLoop, SAVE_STATE_VERSION, VideoMode,
|
||||||
|
VideoOutput, button_pressed, set_button_pressed,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
#[cfg(feature = "adapter-api")]
|
||||||
|
pub use crate::{AudioAdapter, ClockAdapter, InputAdapter, VideoAdapter};
|
||||||
|
pub use crate::{
|
||||||
|
AudioOutput, ClientRuntime, EmulationState, HostConfig, InputProvider, JoypadButton,
|
||||||
|
JoypadButtons, NesRuntime, RuntimeError, RuntimeHostLoop, VideoOutput, button_pressed,
|
||||||
|
create_mapper, parse_rom, set_button_pressed,
|
||||||
|
};
|
||||||
|
}
|
||||||
264
src/native_core/apu/api.rs
Normal file
264
src/native_core/apu/api.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
use super::types::{Apu, ApuStateTail};
|
||||||
|
|
||||||
|
impl Apu {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
io: [0; 0x20],
|
||||||
|
frame_cycle: 0,
|
||||||
|
frame_mode_5step: false,
|
||||||
|
frame_irq_inhibit: false,
|
||||||
|
frame_irq_pending: false,
|
||||||
|
channel_enable_mask: 0,
|
||||||
|
length_counters: [0; 4],
|
||||||
|
dmc_bytes_remaining: 0,
|
||||||
|
dmc_irq_enabled: false,
|
||||||
|
dmc_irq_pending: false,
|
||||||
|
dmc_cycle_counter: 0,
|
||||||
|
dmc_current_addr: 0xC000,
|
||||||
|
dmc_sample_buffer: 0,
|
||||||
|
dmc_sample_buffer_valid: false,
|
||||||
|
dmc_shift_reg: 0,
|
||||||
|
dmc_bits_remaining: 8,
|
||||||
|
dmc_silence: true,
|
||||||
|
dmc_output_level: 0,
|
||||||
|
dmc_dma_request: false,
|
||||||
|
envelope_divider: [0; 3],
|
||||||
|
envelope_decay: [0; 3],
|
||||||
|
envelope_start_flags: 0,
|
||||||
|
triangle_linear_counter: 0,
|
||||||
|
triangle_linear_reload_flag: false,
|
||||||
|
sweep_divider: [0; 2],
|
||||||
|
sweep_reload_flags: 0,
|
||||||
|
cpu_cycle_parity: false,
|
||||||
|
frame_reset_pending: false,
|
||||||
|
frame_reset_delay: 0,
|
||||||
|
pending_frame_mode_5step: false,
|
||||||
|
pending_frame_irq_inhibit: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn registers(&self) -> &[u8; 0x20] {
|
||||||
|
&self.io
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_registers(&mut self, regs: [u8; 0x20]) {
|
||||||
|
self.io = regs;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
match addr {
|
||||||
|
0x4000..=0x4013 => self.io[(addr as usize) - 0x4000],
|
||||||
|
0x4015 => self.read_status(),
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x4000..=0x4013 => {
|
||||||
|
self.io[(addr as usize) - 0x4000] = value;
|
||||||
|
match addr {
|
||||||
|
0x4003 => {
|
||||||
|
self.reload_length_counter(0, value >> 3);
|
||||||
|
self.envelope_start_flags |= 1 << 0;
|
||||||
|
}
|
||||||
|
0x4001 => {
|
||||||
|
self.sweep_reload_flags |= 1 << 0;
|
||||||
|
}
|
||||||
|
0x4007 => {
|
||||||
|
self.reload_length_counter(1, value >> 3);
|
||||||
|
self.envelope_start_flags |= 1 << 1;
|
||||||
|
}
|
||||||
|
0x4005 => {
|
||||||
|
self.sweep_reload_flags |= 1 << 1;
|
||||||
|
}
|
||||||
|
0x400B => {
|
||||||
|
self.reload_length_counter(2, value >> 3);
|
||||||
|
self.triangle_linear_reload_flag = true;
|
||||||
|
}
|
||||||
|
0x400F => {
|
||||||
|
self.reload_length_counter(3, value >> 3);
|
||||||
|
self.envelope_start_flags |= 1 << 2;
|
||||||
|
}
|
||||||
|
0x4010 => {
|
||||||
|
self.dmc_irq_enabled = (value & 0x80) != 0;
|
||||||
|
if !self.dmc_irq_enabled {
|
||||||
|
self.dmc_irq_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x4011 => {
|
||||||
|
self.dmc_output_level = value & 0x7F;
|
||||||
|
}
|
||||||
|
0x4012 => {
|
||||||
|
self.dmc_current_addr = self.dmc_sample_start_addr();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x4014 => {
|
||||||
|
self.io[0x14] = value;
|
||||||
|
}
|
||||||
|
0x4015 => {
|
||||||
|
self.io[(addr as usize) - 0x4000] = value;
|
||||||
|
self.channel_enable_mask = value & 0x1F;
|
||||||
|
self.dmc_irq_pending = false;
|
||||||
|
for chan in 0..4usize {
|
||||||
|
if (self.channel_enable_mask & (1 << chan)) == 0 {
|
||||||
|
self.length_counters[chan] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (self.channel_enable_mask & 0x10) == 0 {
|
||||||
|
self.dmc_bytes_remaining = 0;
|
||||||
|
self.dmc_cycle_counter = 0;
|
||||||
|
self.dmc_dma_request = false;
|
||||||
|
} else if self.dmc_bytes_remaining == 0 {
|
||||||
|
self.dmc_bytes_remaining = self.dmc_sample_length_bytes();
|
||||||
|
self.dmc_cycle_counter = self.dmc_byte_period();
|
||||||
|
self.dmc_current_addr = self.dmc_sample_start_addr();
|
||||||
|
if !self.dmc_sample_buffer_valid {
|
||||||
|
self.dmc_dma_request = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x4017 => {
|
||||||
|
self.io[(addr as usize) - 0x4000] = value;
|
||||||
|
self.pending_frame_mode_5step = (value & 0x80) != 0;
|
||||||
|
self.pending_frame_irq_inhibit = (value & 0x40) != 0;
|
||||||
|
self.frame_reset_delay = if self.cpu_cycle_parity { 4 } else { 3 };
|
||||||
|
self.frame_reset_pending = true;
|
||||||
|
if self.pending_frame_irq_inhibit {
|
||||||
|
self.frame_irq_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_cpu_cycle(&mut self) {
|
||||||
|
let mut skip_frame_counter_clock = false;
|
||||||
|
if self.frame_reset_pending {
|
||||||
|
self.frame_reset_delay = self.frame_reset_delay.saturating_sub(1);
|
||||||
|
if self.frame_reset_delay == 0 {
|
||||||
|
self.frame_reset_pending = false;
|
||||||
|
self.frame_mode_5step = self.pending_frame_mode_5step;
|
||||||
|
self.frame_irq_inhibit = self.pending_frame_irq_inhibit;
|
||||||
|
self.frame_cycle = 0;
|
||||||
|
if self.frame_mode_5step {
|
||||||
|
self.clock_quarter_frame();
|
||||||
|
self.clock_half_frame();
|
||||||
|
}
|
||||||
|
skip_frame_counter_clock = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !skip_frame_counter_clock {
|
||||||
|
self.clock_frame_counter();
|
||||||
|
}
|
||||||
|
self.clock_dmc();
|
||||||
|
self.cpu_cycle_parity = !self.cpu_cycle_parity;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll_irq(&self) -> bool {
|
||||||
|
self.dmc_irq_pending || (self.frame_irq_pending && !self.frame_irq_inhibit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_dmc_dma_request(&mut self) -> Option<u16> {
|
||||||
|
if self.dmc_dma_request {
|
||||||
|
Some(self.dmc_current_addr)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn provide_dmc_dma_byte(&mut self, byte: u8) {
|
||||||
|
if !self.dmc_dma_request {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.dmc_dma_request = false;
|
||||||
|
self.dmc_sample_buffer = byte;
|
||||||
|
self.dmc_sample_buffer_valid = true;
|
||||||
|
self.dmc_current_addr = if self.dmc_current_addr == 0xFFFF {
|
||||||
|
0x8000
|
||||||
|
} else {
|
||||||
|
self.dmc_current_addr.wrapping_add(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.dmc_bytes_remaining > 0 {
|
||||||
|
self.dmc_bytes_remaining -= 1;
|
||||||
|
}
|
||||||
|
if self.dmc_bytes_remaining == 0 {
|
||||||
|
if (self.io[0x10] & 0x40) != 0 {
|
||||||
|
self.dmc_bytes_remaining = self.dmc_sample_length_bytes();
|
||||||
|
self.dmc_current_addr = self.dmc_sample_start_addr();
|
||||||
|
self.dmc_dma_request = true;
|
||||||
|
} else if self.dmc_irq_enabled {
|
||||||
|
self.dmc_irq_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_state_tail(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.frame_cycle.to_le_bytes());
|
||||||
|
out.push(u8::from(self.frame_mode_5step));
|
||||||
|
out.push(u8::from(self.frame_irq_inhibit));
|
||||||
|
out.push(u8::from(self.frame_irq_pending));
|
||||||
|
out.push(self.channel_enable_mask);
|
||||||
|
out.extend_from_slice(&self.length_counters);
|
||||||
|
out.extend_from_slice(&self.dmc_bytes_remaining.to_le_bytes());
|
||||||
|
out.push(u8::from(self.dmc_irq_enabled));
|
||||||
|
out.push(u8::from(self.dmc_irq_pending));
|
||||||
|
out.extend_from_slice(&self.dmc_cycle_counter.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.dmc_current_addr.to_le_bytes());
|
||||||
|
out.push(self.dmc_sample_buffer);
|
||||||
|
out.push(u8::from(self.dmc_sample_buffer_valid));
|
||||||
|
out.push(self.dmc_shift_reg);
|
||||||
|
out.push(self.dmc_bits_remaining);
|
||||||
|
out.push(u8::from(self.dmc_silence));
|
||||||
|
out.push(self.dmc_output_level);
|
||||||
|
out.push(u8::from(self.dmc_dma_request));
|
||||||
|
out.extend_from_slice(&self.envelope_divider);
|
||||||
|
out.extend_from_slice(&self.envelope_decay);
|
||||||
|
out.push(self.envelope_start_flags);
|
||||||
|
out.push(self.triangle_linear_counter);
|
||||||
|
out.push(u8::from(self.triangle_linear_reload_flag));
|
||||||
|
out.extend_from_slice(&self.sweep_divider);
|
||||||
|
out.push(self.sweep_reload_flags);
|
||||||
|
out.push(u8::from(self.cpu_cycle_parity));
|
||||||
|
out.push(u8::from(self.frame_reset_pending));
|
||||||
|
out.push(self.frame_reset_delay);
|
||||||
|
out.push(u8::from(self.pending_frame_mode_5step));
|
||||||
|
out.push(u8::from(self.pending_frame_irq_inhibit));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_state_tail(&mut self, state: ApuStateTail) {
|
||||||
|
self.frame_cycle = state.frame_cycle;
|
||||||
|
self.frame_mode_5step = state.frame_mode_5step;
|
||||||
|
self.frame_irq_inhibit = state.frame_irq_inhibit;
|
||||||
|
self.frame_irq_pending = state.frame_irq_pending;
|
||||||
|
self.channel_enable_mask = state.channel_enable_mask;
|
||||||
|
self.length_counters = state.length_counters;
|
||||||
|
self.dmc_bytes_remaining = state.dmc_bytes_remaining;
|
||||||
|
self.dmc_irq_enabled = state.dmc_irq_enabled;
|
||||||
|
self.dmc_irq_pending = state.dmc_irq_pending;
|
||||||
|
self.dmc_cycle_counter = state.dmc_cycle_counter;
|
||||||
|
self.dmc_current_addr = state.dmc_current_addr;
|
||||||
|
self.dmc_sample_buffer = state.dmc_sample_buffer;
|
||||||
|
self.dmc_sample_buffer_valid = state.dmc_sample_buffer_valid;
|
||||||
|
self.dmc_shift_reg = state.dmc_shift_reg;
|
||||||
|
self.dmc_bits_remaining = state.dmc_bits_remaining.max(1);
|
||||||
|
self.dmc_silence = state.dmc_silence;
|
||||||
|
self.dmc_output_level = state.dmc_output_level & 0x7F;
|
||||||
|
self.dmc_dma_request = state.dmc_dma_request;
|
||||||
|
self.envelope_divider = state.envelope_divider;
|
||||||
|
self.envelope_decay = state.envelope_decay;
|
||||||
|
self.envelope_start_flags = state.envelope_start_flags & 0x07;
|
||||||
|
self.triangle_linear_counter = state.triangle_linear_counter & 0x7F;
|
||||||
|
self.triangle_linear_reload_flag = state.triangle_linear_reload_flag;
|
||||||
|
self.sweep_divider = state.sweep_divider;
|
||||||
|
self.sweep_reload_flags = state.sweep_reload_flags & 0x03;
|
||||||
|
self.cpu_cycle_parity = state.cpu_cycle_parity;
|
||||||
|
self.frame_reset_pending = state.frame_reset_pending;
|
||||||
|
self.frame_reset_delay = state.frame_reset_delay;
|
||||||
|
self.pending_frame_mode_5step = state.pending_frame_mode_5step;
|
||||||
|
self.pending_frame_irq_inhibit = state.pending_frame_irq_inhibit;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/native_core/apu/mod.rs
Normal file
5
src/native_core/apu/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod api;
|
||||||
|
mod timing;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub use types::{Apu, ApuStateTail};
|
||||||
227
src/native_core/apu/timing.rs
Normal file
227
src/native_core/apu/timing.rs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
impl Apu {
|
||||||
|
pub(crate) fn read_status(&mut self) -> u8 {
|
||||||
|
let mut status = 0u8;
|
||||||
|
status |= u8::from(self.length_counters[0] > 0);
|
||||||
|
status |= u8::from(self.length_counters[1] > 0) << 1;
|
||||||
|
status |= u8::from(self.length_counters[2] > 0) << 2;
|
||||||
|
status |= u8::from(self.length_counters[3] > 0) << 3;
|
||||||
|
status |= u8::from(self.dmc_bytes_remaining > 0) << 4;
|
||||||
|
status |= u8::from(self.frame_irq_pending) << 6;
|
||||||
|
status |= u8::from(self.dmc_irq_pending) << 7;
|
||||||
|
self.frame_irq_pending = false;
|
||||||
|
status
|
||||||
|
}
|
||||||
|
pub(crate) fn clock_frame_counter(&mut self) {
|
||||||
|
let seq_len = if self.frame_mode_5step {
|
||||||
|
APU_FRAME_SEQ_5_STEP_CYCLES
|
||||||
|
} else {
|
||||||
|
APU_FRAME_SEQ_4_STEP_CYCLES
|
||||||
|
};
|
||||||
|
if seq_len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.frame_cycle = self.frame_cycle.wrapping_add(1);
|
||||||
|
if self.frame_cycle == APU_HALF_FRAME_1
|
||||||
|
|| (!self.frame_mode_5step && self.frame_cycle == APU_HALF_FRAME_2_4STEP)
|
||||||
|
|| (self.frame_mode_5step && self.frame_cycle == APU_HALF_FRAME_2_5STEP)
|
||||||
|
{
|
||||||
|
self.clock_half_frame();
|
||||||
|
}
|
||||||
|
let quarter_frame_tick = self.frame_cycle == APU_QUARTER_FRAME_1
|
||||||
|
|| self.frame_cycle == APU_QUARTER_FRAME_2
|
||||||
|
|| self.frame_cycle == APU_QUARTER_FRAME_3
|
||||||
|
|| (!self.frame_mode_5step && self.frame_cycle == APU_QUARTER_FRAME_4)
|
||||||
|
|| (self.frame_mode_5step && self.frame_cycle == APU_QUARTER_FRAME_5STEP_4);
|
||||||
|
if quarter_frame_tick {
|
||||||
|
self.clock_quarter_frame();
|
||||||
|
}
|
||||||
|
if !self.frame_mode_5step
|
||||||
|
&& self.frame_cycle == APU_FRAME_SEQ_4_STEP_CYCLES - 1
|
||||||
|
&& !self.frame_irq_inhibit
|
||||||
|
{
|
||||||
|
self.frame_irq_pending = true;
|
||||||
|
}
|
||||||
|
if self.frame_cycle >= seq_len {
|
||||||
|
self.frame_cycle = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn clock_half_frame(&mut self) {
|
||||||
|
self.clock_sweep(0, 0x01);
|
||||||
|
self.clock_sweep(1, 0x05);
|
||||||
|
|
||||||
|
for chan in 0..4usize {
|
||||||
|
if self.length_counters[chan] == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.length_halt(chan) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.length_counters[chan] -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn clock_quarter_frame(&mut self) {
|
||||||
|
self.clock_envelope(0, 0x00);
|
||||||
|
self.clock_envelope(1, 0x04);
|
||||||
|
self.clock_envelope(2, 0x0C);
|
||||||
|
|
||||||
|
if self.triangle_linear_reload_flag {
|
||||||
|
self.triangle_linear_counter = self.io[0x08] & 0x7F;
|
||||||
|
} else if self.triangle_linear_counter > 0 {
|
||||||
|
self.triangle_linear_counter -= 1;
|
||||||
|
}
|
||||||
|
if (self.io[0x08] & 0x80) == 0 {
|
||||||
|
self.triangle_linear_reload_flag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn clock_envelope(&mut self, env_idx: usize, reg_idx: usize) {
|
||||||
|
let start_mask = 1u8 << env_idx;
|
||||||
|
let period = self.io[reg_idx] & 0x0F;
|
||||||
|
let loop_flag = (self.io[reg_idx] & 0x20) != 0;
|
||||||
|
if (self.envelope_start_flags & start_mask) != 0 {
|
||||||
|
self.envelope_start_flags &= !start_mask;
|
||||||
|
self.envelope_decay[env_idx] = 15;
|
||||||
|
self.envelope_divider[env_idx] = period;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.envelope_divider[env_idx] == 0 {
|
||||||
|
self.envelope_divider[env_idx] = period;
|
||||||
|
if self.envelope_decay[env_idx] == 0 {
|
||||||
|
if loop_flag {
|
||||||
|
self.envelope_decay[env_idx] = 15;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.envelope_decay[env_idx] -= 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.envelope_divider[env_idx] -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn clock_dmc(&mut self) {
|
||||||
|
if (self.channel_enable_mask & 0x10) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.dmc_cycle_counter == 0 {
|
||||||
|
self.dmc_cycle_counter = self.dmc_byte_period();
|
||||||
|
}
|
||||||
|
self.dmc_cycle_counter = self.dmc_cycle_counter.saturating_sub(1);
|
||||||
|
if self.dmc_cycle_counter != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.dmc_cycle_counter = self.dmc_byte_period();
|
||||||
|
|
||||||
|
if !self.dmc_silence {
|
||||||
|
if (self.dmc_shift_reg & 0x01) != 0 {
|
||||||
|
self.dmc_output_level = self.dmc_output_level.saturating_add(2).min(127);
|
||||||
|
} else {
|
||||||
|
self.dmc_output_level = self.dmc_output_level.saturating_sub(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.dmc_shift_reg >>= 1;
|
||||||
|
self.dmc_bits_remaining = self.dmc_bits_remaining.saturating_sub(1);
|
||||||
|
|
||||||
|
if self.dmc_bits_remaining == 0 {
|
||||||
|
self.dmc_bits_remaining = 8;
|
||||||
|
if self.dmc_sample_buffer_valid {
|
||||||
|
self.dmc_shift_reg = self.dmc_sample_buffer;
|
||||||
|
self.dmc_sample_buffer_valid = false;
|
||||||
|
self.dmc_silence = false;
|
||||||
|
} else {
|
||||||
|
self.dmc_silence = true;
|
||||||
|
}
|
||||||
|
if self.dmc_bytes_remaining > 0
|
||||||
|
&& !self.dmc_sample_buffer_valid
|
||||||
|
&& !self.dmc_dma_request
|
||||||
|
{
|
||||||
|
self.dmc_dma_request = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn length_halt(&self, channel: usize) -> bool {
|
||||||
|
match channel {
|
||||||
|
0 => (self.io[0x00] & 0x20) != 0,
|
||||||
|
1 => (self.io[0x04] & 0x20) != 0,
|
||||||
|
2 => (self.io[0x08] & 0x80) != 0,
|
||||||
|
3 => (self.io[0x0C] & 0x20) != 0,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn reload_length_counter(&mut self, channel: usize, length_index: u8) {
|
||||||
|
if channel >= self.length_counters.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (self.channel_enable_mask & (1 << channel)) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.length_counters[channel] = APU_LENGTH_TABLE[(length_index & 0x1F) as usize];
|
||||||
|
}
|
||||||
|
pub(crate) fn dmc_sample_length_bytes(&self) -> u16 {
|
||||||
|
(self.io[0x13] as u16).saturating_mul(16).saturating_add(1)
|
||||||
|
}
|
||||||
|
pub(crate) fn dmc_sample_start_addr(&self) -> u16 {
|
||||||
|
0xC000u16.saturating_add((self.io[0x12] as u16) << 6)
|
||||||
|
}
|
||||||
|
pub(crate) fn dmc_byte_period(&self) -> u16 {
|
||||||
|
let rate_idx = (self.io[0x10] & 0x0F) as usize;
|
||||||
|
APU_DMC_PERIOD_TABLE[rate_idx]
|
||||||
|
}
|
||||||
|
pub(crate) fn clock_sweep(&mut self, channel: usize, reg_idx: usize) {
|
||||||
|
let reload_mask = 1u8 << channel;
|
||||||
|
let sweep_reg = self.io[reg_idx];
|
||||||
|
let enabled = (sweep_reg & 0x80) != 0;
|
||||||
|
let period = ((sweep_reg >> 4) & 0x07).saturating_add(1);
|
||||||
|
let shift = sweep_reg & 0x07;
|
||||||
|
let divider = self.sweep_divider[channel];
|
||||||
|
let reload = (self.sweep_reload_flags & reload_mask) != 0;
|
||||||
|
let mute = self.sweep_mutes_channel(channel, reg_idx + 1);
|
||||||
|
|
||||||
|
if divider == 0 && enabled && shift != 0 && !mute {
|
||||||
|
let target = self.sweep_target_period(channel, reg_idx + 1);
|
||||||
|
if target <= 0x07FF {
|
||||||
|
self.set_pulse_timer_period(channel, target as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if divider == 0 || reload {
|
||||||
|
self.sweep_divider[channel] = period;
|
||||||
|
} else {
|
||||||
|
self.sweep_divider[channel] = divider.saturating_sub(1);
|
||||||
|
}
|
||||||
|
self.sweep_reload_flags &= !reload_mask;
|
||||||
|
}
|
||||||
|
pub(crate) fn sweep_target_period(&self, channel: usize, timer_lo_idx: usize) -> i32 {
|
||||||
|
let period = self.pulse_timer_period(timer_lo_idx) as i32;
|
||||||
|
let shift = (self.io[timer_lo_idx - 1] & 0x07) as i32;
|
||||||
|
if shift == 0 {
|
||||||
|
return period;
|
||||||
|
}
|
||||||
|
let change = period >> shift;
|
||||||
|
if (self.io[timer_lo_idx - 1] & 0x08) != 0 {
|
||||||
|
if channel == 0 {
|
||||||
|
period - change - 1
|
||||||
|
} else {
|
||||||
|
period - change
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
period + change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn sweep_mutes_channel(&self, channel: usize, timer_lo_idx: usize) -> bool {
|
||||||
|
let period = self.pulse_timer_period(timer_lo_idx);
|
||||||
|
period < 8 || self.sweep_target_period(channel, timer_lo_idx) > 0x07FF
|
||||||
|
}
|
||||||
|
pub(crate) fn pulse_timer_period(&self, timer_lo_idx: usize) -> u16 {
|
||||||
|
let lo = self.io[timer_lo_idx] as u16;
|
||||||
|
let hi = (self.io[timer_lo_idx + 1] as u16 & 0x07) << 8;
|
||||||
|
hi | lo
|
||||||
|
}
|
||||||
|
pub(crate) fn set_pulse_timer_period(&mut self, channel: usize, period: u16) {
|
||||||
|
let (timer_lo_idx, timer_hi_idx) = if channel == 0 {
|
||||||
|
(0x02, 0x03)
|
||||||
|
} else {
|
||||||
|
(0x06, 0x07)
|
||||||
|
};
|
||||||
|
self.io[timer_lo_idx] = (period & 0x00FF) as u8;
|
||||||
|
self.io[timer_hi_idx] = (self.io[timer_hi_idx] & !0x07) | ((period >> 8) as u8 & 0x07);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/native_core/apu/types.rs
Normal file
90
src/native_core/apu/types.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
pub(super) const APU_FRAME_SEQ_4_STEP_CYCLES: u32 = 14_915;
|
||||||
|
pub(super) const APU_FRAME_SEQ_5_STEP_CYCLES: u32 = 18_641;
|
||||||
|
pub(super) const APU_QUARTER_FRAME_1: u32 = 3_729;
|
||||||
|
pub(super) const APU_QUARTER_FRAME_2: u32 = 7_457;
|
||||||
|
pub(super) const APU_QUARTER_FRAME_3: u32 = 11_186;
|
||||||
|
pub(super) const APU_QUARTER_FRAME_4: u32 = 14_915;
|
||||||
|
pub(super) const APU_QUARTER_FRAME_5STEP_4: u32 = 18_641;
|
||||||
|
pub(super) const APU_HALF_FRAME_1: u32 = 7_457;
|
||||||
|
pub(super) const APU_HALF_FRAME_2_4STEP: u32 = 14_915;
|
||||||
|
pub(super) const APU_HALF_FRAME_2_5STEP: u32 = 18_641;
|
||||||
|
pub(super) const APU_DMC_PERIOD_TABLE: [u16; 16] = [
|
||||||
|
428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 85, 72, 54,
|
||||||
|
];
|
||||||
|
pub(super) const APU_LENGTH_TABLE: [u8; 32] = [
|
||||||
|
10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
|
||||||
|
192, 24, 72, 26, 16, 28, 32, 30,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct Apu {
|
||||||
|
pub(super) io: [u8; 0x20],
|
||||||
|
pub(crate) frame_cycle: u32,
|
||||||
|
pub(crate) frame_mode_5step: bool,
|
||||||
|
pub(crate) frame_irq_inhibit: bool,
|
||||||
|
pub(crate) frame_irq_pending: bool,
|
||||||
|
pub(crate) channel_enable_mask: u8,
|
||||||
|
pub(crate) length_counters: [u8; 4],
|
||||||
|
pub(crate) dmc_bytes_remaining: u16,
|
||||||
|
pub(crate) dmc_irq_enabled: bool,
|
||||||
|
pub(crate) dmc_irq_pending: bool,
|
||||||
|
pub(crate) dmc_cycle_counter: u16,
|
||||||
|
pub(crate) dmc_current_addr: u16,
|
||||||
|
pub(crate) dmc_sample_buffer: u8,
|
||||||
|
pub(crate) dmc_sample_buffer_valid: bool,
|
||||||
|
pub(crate) dmc_shift_reg: u8,
|
||||||
|
pub(crate) dmc_bits_remaining: u8,
|
||||||
|
pub(crate) dmc_silence: bool,
|
||||||
|
pub(crate) dmc_output_level: u8,
|
||||||
|
pub(crate) dmc_dma_request: bool,
|
||||||
|
pub(crate) envelope_divider: [u8; 3],
|
||||||
|
pub(crate) envelope_decay: [u8; 3],
|
||||||
|
pub(crate) envelope_start_flags: u8,
|
||||||
|
pub(crate) triangle_linear_counter: u8,
|
||||||
|
pub(crate) triangle_linear_reload_flag: bool,
|
||||||
|
pub(crate) sweep_divider: [u8; 2],
|
||||||
|
pub(crate) sweep_reload_flags: u8,
|
||||||
|
pub(crate) cpu_cycle_parity: bool,
|
||||||
|
pub(crate) frame_reset_pending: bool,
|
||||||
|
pub(crate) frame_reset_delay: u8,
|
||||||
|
pub(crate) pending_frame_mode_5step: bool,
|
||||||
|
pub(crate) pending_frame_irq_inhibit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApuStateTail {
|
||||||
|
pub frame_cycle: u32,
|
||||||
|
pub frame_mode_5step: bool,
|
||||||
|
pub frame_irq_inhibit: bool,
|
||||||
|
pub frame_irq_pending: bool,
|
||||||
|
pub channel_enable_mask: u8,
|
||||||
|
pub length_counters: [u8; 4],
|
||||||
|
pub dmc_bytes_remaining: u16,
|
||||||
|
pub dmc_irq_enabled: bool,
|
||||||
|
pub dmc_irq_pending: bool,
|
||||||
|
pub dmc_cycle_counter: u16,
|
||||||
|
pub dmc_current_addr: u16,
|
||||||
|
pub dmc_sample_buffer: u8,
|
||||||
|
pub dmc_sample_buffer_valid: bool,
|
||||||
|
pub dmc_shift_reg: u8,
|
||||||
|
pub dmc_bits_remaining: u8,
|
||||||
|
pub dmc_silence: bool,
|
||||||
|
pub dmc_output_level: u8,
|
||||||
|
pub dmc_dma_request: bool,
|
||||||
|
pub envelope_divider: [u8; 3],
|
||||||
|
pub envelope_decay: [u8; 3],
|
||||||
|
pub envelope_start_flags: u8,
|
||||||
|
pub triangle_linear_counter: u8,
|
||||||
|
pub triangle_linear_reload_flag: bool,
|
||||||
|
pub sweep_divider: [u8; 2],
|
||||||
|
pub sweep_reload_flags: u8,
|
||||||
|
pub cpu_cycle_parity: bool,
|
||||||
|
pub frame_reset_pending: bool,
|
||||||
|
pub frame_reset_delay: u8,
|
||||||
|
pub pending_frame_mode_5step: bool,
|
||||||
|
pub pending_frame_irq_inhibit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Apu {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/native_core/bus.rs
Normal file
93
src/native_core/bus.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use crate::native_core::{apu::Apu, cpu::CpuBus, mapper::Mapper, ppu::Ppu};
|
||||||
|
|
||||||
|
const CPU_RAM_SIZE: usize = 0x0800;
|
||||||
|
const PPU_DOTS_PER_SCANLINE: u32 = 341;
|
||||||
|
const PPU_SCANLINES_PER_FRAME: u32 = 262;
|
||||||
|
const PPU_DOTS_PER_FRAME: u32 = PPU_DOTS_PER_SCANLINE * PPU_SCANLINES_PER_FRAME;
|
||||||
|
const PPU_VBLANK_START_SCANLINE: u32 = 241;
|
||||||
|
const PPU_PRERENDER_SCANLINE: u32 = 261;
|
||||||
|
|
||||||
|
pub struct NativeBus {
|
||||||
|
cpu_ram: [u8; CPU_RAM_SIZE],
|
||||||
|
ppu: Ppu,
|
||||||
|
apu: Apu,
|
||||||
|
cpu_open_bus: u8,
|
||||||
|
joypad_state: u8,
|
||||||
|
joypad_shift: u8,
|
||||||
|
joypad2_state: u8,
|
||||||
|
joypad2_shift: u8,
|
||||||
|
joypad_strobe: bool,
|
||||||
|
nmi_pending: bool,
|
||||||
|
suppress_vblank_this_frame: bool,
|
||||||
|
ppu_dot: u32,
|
||||||
|
odd_frame: bool,
|
||||||
|
in_vblank: bool,
|
||||||
|
frame_complete: bool,
|
||||||
|
mmc3_a12_prev_high: bool,
|
||||||
|
mmc3_a12_low_dots: u16,
|
||||||
|
mmc3_last_irq_scanline: u32,
|
||||||
|
mapper: Box<dyn Mapper + Send>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeBus {
|
||||||
|
pub fn new(mapper: Box<dyn Mapper + Send>) -> Self {
|
||||||
|
Self {
|
||||||
|
cpu_ram: [0; CPU_RAM_SIZE],
|
||||||
|
ppu: Ppu::new(),
|
||||||
|
apu: Apu::new(),
|
||||||
|
cpu_open_bus: 0,
|
||||||
|
joypad_state: 0,
|
||||||
|
joypad_shift: 0,
|
||||||
|
joypad2_state: 0,
|
||||||
|
joypad2_shift: 0,
|
||||||
|
joypad_strobe: false,
|
||||||
|
nmi_pending: false,
|
||||||
|
suppress_vblank_this_frame: false,
|
||||||
|
ppu_dot: 0,
|
||||||
|
odd_frame: false,
|
||||||
|
in_vblank: false,
|
||||||
|
frame_complete: false,
|
||||||
|
mmc3_a12_prev_high: false,
|
||||||
|
mmc3_a12_low_dots: 8,
|
||||||
|
mmc3_last_irq_scanline: u32::MAX,
|
||||||
|
mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apu_registers(&self) -> &[u8; 0x20] {
|
||||||
|
self.apu.registers()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_frame(&self, out_rgba: &mut [u8], frame_number: u32, buttons: [bool; 8]) {
|
||||||
|
let _ = (frame_number, buttons);
|
||||||
|
let src = self.ppu.frame_buffer();
|
||||||
|
if out_rgba.len() >= src.len() {
|
||||||
|
out_rgba[..src.len()].copy_from_slice(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_frame(&mut self) {
|
||||||
|
self.frame_complete = false;
|
||||||
|
self.ppu.begin_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_frame_complete(&mut self) -> bool {
|
||||||
|
let out = self.frame_complete;
|
||||||
|
self.frame_complete = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
self.clock_cpu_cycles(cycles as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CpuBus trait implementation (memory map + side effects).
|
||||||
|
mod cpu_bus_impl;
|
||||||
|
mod joypad;
|
||||||
|
// Save-state serialization helpers.
|
||||||
|
mod state;
|
||||||
|
mod timing;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
101
src/native_core/bus/cpu_bus_impl.rs
Normal file
101
src/native_core/bus/cpu_bus_impl.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
use super::*;
|
||||||
|
impl CpuBus for NativeBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
let value = match addr {
|
||||||
|
0x0000..=0x1FFF => self.cpu_ram[(addr as usize) & 0x07FF],
|
||||||
|
0x2000..=0x3FFF => {
|
||||||
|
let reg = (addr as u8) & 7;
|
||||||
|
let scanline = self.ppu_dot / PPU_DOTS_PER_SCANLINE;
|
||||||
|
let dot = self.ppu_dot % PPU_DOTS_PER_SCANLINE;
|
||||||
|
let value = if reg == 4 {
|
||||||
|
let rendering_read_phase = self.ppu.rendering_enabled()
|
||||||
|
&& (scanline < 240 || scanline == PPU_PRERENDER_SCANLINE)
|
||||||
|
&& ((1..=256).contains(&dot) || (321..=340).contains(&dot));
|
||||||
|
self.ppu.cpu_read_oamdata(rendering_read_phase)
|
||||||
|
} else {
|
||||||
|
let mapper: &(dyn Mapper + Send) = &*self.mapper;
|
||||||
|
self.ppu.cpu_read(reg, mapper)
|
||||||
|
};
|
||||||
|
if reg == 2 {
|
||||||
|
if scanline == PPU_VBLANK_START_SCANLINE && dot == 1 {
|
||||||
|
self.suppress_vblank_this_frame = true;
|
||||||
|
}
|
||||||
|
// Reading PPUSTATUS clears VBlank; do not keep a stale NMI latched.
|
||||||
|
self.nmi_pending = false;
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
0x4000..=0x4013 => self.cpu_open_bus,
|
||||||
|
0x4015 => self.apu.read(addr),
|
||||||
|
0x4016 => self.joypad_read(),
|
||||||
|
0x4017 => self.joypad2_read(),
|
||||||
|
0x6000..=0x7FFF => self.mapper.cpu_read_low(addr).unwrap_or(self.cpu_open_bus),
|
||||||
|
0x8000..=0xFFFF => self.mapper.cpu_read(addr),
|
||||||
|
_ => self.cpu_open_bus,
|
||||||
|
};
|
||||||
|
self.cpu_open_bus = value;
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.cpu_open_bus = value;
|
||||||
|
match addr {
|
||||||
|
0x0000..=0x1FFF => self.cpu_ram[(addr as usize) & 0x07FF] = value,
|
||||||
|
0x2000..=0x3FFF => {
|
||||||
|
let reg = (addr as u8) & 7;
|
||||||
|
let nmi_was_enabled = self.ppu.nmi_enabled();
|
||||||
|
{
|
||||||
|
let (ppu, mapper) = (&mut self.ppu, &mut self.mapper);
|
||||||
|
ppu.cpu_write(reg, value, &mut **mapper);
|
||||||
|
}
|
||||||
|
if reg == 0
|
||||||
|
&& !nmi_was_enabled
|
||||||
|
&& self.ppu.nmi_enabled()
|
||||||
|
&& self.ppu.vblank_flag_set()
|
||||||
|
{
|
||||||
|
self.nmi_pending = true;
|
||||||
|
}
|
||||||
|
if reg == 0 || reg == 5 {
|
||||||
|
self.note_scroll_write_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x4000..=0x4013 => self.apu.write(addr, value),
|
||||||
|
0x4015 => self.apu.write(addr, value),
|
||||||
|
0x4017 => self.apu.write(addr, value),
|
||||||
|
0x4014 => {
|
||||||
|
self.apu.write(0x4014, value);
|
||||||
|
let base = (value as u16) << 8;
|
||||||
|
let mut dma = [0u8; 256];
|
||||||
|
for i in 0..=u8::MAX {
|
||||||
|
dma[i as usize] = self.dma_read(base.wrapping_add(i as u16));
|
||||||
|
}
|
||||||
|
for byte in dma {
|
||||||
|
self.ppu.dma_write_oam(byte);
|
||||||
|
}
|
||||||
|
// OAM DMA stalls CPU for 513/514 cycles while PPU and mapper continue.
|
||||||
|
let cpu_phase = (self.ppu_dot / 3) & 1;
|
||||||
|
self.clock_cpu_cycles(513 + cpu_phase);
|
||||||
|
}
|
||||||
|
0x4016 => self.joypad_write(value),
|
||||||
|
0x6000..=0x7FFF => {
|
||||||
|
self.mapper.cpu_write_low(addr, value);
|
||||||
|
}
|
||||||
|
0x8000..=0xFFFF => self.mapper.cpu_write(addr, value),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_nmi(&mut self) -> bool {
|
||||||
|
let out = self.nmi_pending;
|
||||||
|
self.nmi_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
if self.apu.poll_irq() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
self.mapper.poll_irq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/native_core/bus/joypad.rs
Normal file
74
src/native_core/bus/joypad.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use super::NativeBus;
|
||||||
|
|
||||||
|
impl NativeBus {
|
||||||
|
pub fn set_joypad_buttons(&mut self, buttons: [bool; 8]) {
|
||||||
|
let mut state = 0u8;
|
||||||
|
if buttons[4] {
|
||||||
|
state |= 1 << 0; // A
|
||||||
|
}
|
||||||
|
if buttons[5] {
|
||||||
|
state |= 1 << 1; // B
|
||||||
|
}
|
||||||
|
if buttons[7] {
|
||||||
|
state |= 1 << 2; // Select
|
||||||
|
}
|
||||||
|
if buttons[6] {
|
||||||
|
state |= 1 << 3; // Start
|
||||||
|
}
|
||||||
|
if buttons[0] {
|
||||||
|
state |= 1 << 4; // Up
|
||||||
|
}
|
||||||
|
if buttons[1] {
|
||||||
|
state |= 1 << 5; // Down
|
||||||
|
}
|
||||||
|
if buttons[2] {
|
||||||
|
state |= 1 << 6; // Left
|
||||||
|
}
|
||||||
|
if buttons[3] {
|
||||||
|
state |= 1 << 7; // Right
|
||||||
|
}
|
||||||
|
self.joypad_state = state;
|
||||||
|
self.joypad2_state = 0;
|
||||||
|
if self.joypad_strobe {
|
||||||
|
self.joypad_shift = self.joypad_state;
|
||||||
|
self.joypad2_shift = self.joypad2_state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn joypad_read(&mut self) -> u8 {
|
||||||
|
let bit = if self.joypad_strobe {
|
||||||
|
self.joypad_state & 1
|
||||||
|
} else {
|
||||||
|
let bit = self.joypad_shift & 1;
|
||||||
|
self.joypad_shift = (self.joypad_shift >> 1) | 0x80;
|
||||||
|
bit
|
||||||
|
};
|
||||||
|
self.format_controller_read(bit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn joypad2_read(&mut self) -> u8 {
|
||||||
|
let bit = if self.joypad_strobe {
|
||||||
|
self.joypad2_state & 1
|
||||||
|
} else {
|
||||||
|
let bit = self.joypad2_shift & 1;
|
||||||
|
self.joypad2_shift = (self.joypad2_shift >> 1) | 0x80;
|
||||||
|
bit
|
||||||
|
};
|
||||||
|
self.format_controller_read(bit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn joypad_write(&mut self, value: u8) {
|
||||||
|
let strobe = (value & 1) != 0;
|
||||||
|
self.joypad_strobe = strobe;
|
||||||
|
if strobe {
|
||||||
|
self.joypad_shift = self.joypad_state;
|
||||||
|
self.joypad2_shift = self.joypad2_state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_controller_read(&self, bit: u8) -> u8 {
|
||||||
|
// Controller reads expose serial data in bit0, keep bit6 high, and
|
||||||
|
// preserve open-bus upper bits.
|
||||||
|
(self.cpu_open_bus & 0xE0) | 0x40 | (bit & 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
158
src/native_core/bus/state.rs
Normal file
158
src/native_core/bus/state.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::native_core::apu::ApuStateTail;
|
||||||
|
use crate::native_core::state_io as sio;
|
||||||
|
|
||||||
|
const BUS_STATE_CTX: &str = "bus state";
|
||||||
|
|
||||||
|
impl NativeBus {
|
||||||
|
pub fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.cpu_ram);
|
||||||
|
self.ppu.save_state(out);
|
||||||
|
out.extend_from_slice(self.apu.registers());
|
||||||
|
out.push(self.cpu_open_bus);
|
||||||
|
out.push(self.joypad_state);
|
||||||
|
out.push(self.joypad_shift);
|
||||||
|
out.push(self.joypad2_state);
|
||||||
|
out.push(self.joypad2_shift);
|
||||||
|
out.push(u8::from(self.joypad_strobe));
|
||||||
|
out.push(u8::from(self.nmi_pending));
|
||||||
|
out.extend_from_slice(&self.ppu_dot.to_le_bytes());
|
||||||
|
out.push(u8::from(self.odd_frame));
|
||||||
|
out.push(u8::from(self.in_vblank));
|
||||||
|
out.push(u8::from(self.mmc3_a12_prev_high));
|
||||||
|
out.extend_from_slice(&self.mmc3_a12_low_dots.to_le_bytes());
|
||||||
|
out.extend_from_slice(&self.mmc3_last_irq_scanline.to_le_bytes());
|
||||||
|
self.apu.save_state_tail(out);
|
||||||
|
|
||||||
|
let mut mapper_state = Vec::new();
|
||||||
|
self.mapper.save_state(&mut mapper_state);
|
||||||
|
out.extend_from_slice(&(mapper_state.len() as u32).to_le_bytes());
|
||||||
|
out.extend_from_slice(&mapper_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
|
||||||
|
self.cpu_ram.copy_from_slice(sio::take_exact(
|
||||||
|
data,
|
||||||
|
&mut cursor,
|
||||||
|
CPU_RAM_SIZE,
|
||||||
|
BUS_STATE_CTX,
|
||||||
|
)?);
|
||||||
|
let ppu_consumed = self.ppu.load_state(&data[cursor..])?;
|
||||||
|
cursor = cursor.saturating_add(ppu_consumed);
|
||||||
|
let mut apu_regs = [0u8; 0x20];
|
||||||
|
apu_regs.copy_from_slice(sio::take_exact(data, &mut cursor, 0x20, BUS_STATE_CTX)?);
|
||||||
|
self.apu.set_registers(apu_regs);
|
||||||
|
self.frame_complete = false;
|
||||||
|
|
||||||
|
self.cpu_open_bus = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
self.joypad_state = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
self.joypad_shift = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
self.joypad2_state = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
self.joypad2_shift = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
self.joypad_strobe = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
self.nmi_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
self.suppress_vblank_this_frame = false;
|
||||||
|
self.ppu_dot = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
self.ppu_dot %= PPU_DOTS_PER_FRAME;
|
||||||
|
self.odd_frame = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
self.in_vblank = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
self.mmc3_a12_prev_high = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
self.mmc3_a12_low_dots = u16::from_le_bytes([
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
]);
|
||||||
|
self.mmc3_last_irq_scanline = u32::from_le_bytes([
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
]);
|
||||||
|
let frame_cycle = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let frame_mode_5step = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let frame_irq_inhibit = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let frame_irq_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let channel_enable_mask = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let mut length_counters = [0u8; 4];
|
||||||
|
length_counters.copy_from_slice(sio::take_exact(data, &mut cursor, 4, BUS_STATE_CTX)?);
|
||||||
|
let dmc_bytes_remaining = u16::from_le_bytes([
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
]);
|
||||||
|
let dmc_irq_enabled = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let dmc_irq_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let dmc_cycle_counter = u16::from_le_bytes([
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
]);
|
||||||
|
let dmc_current_addr = u16::from_le_bytes([
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?,
|
||||||
|
]);
|
||||||
|
let dmc_sample_buffer = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let dmc_sample_buffer_valid = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let dmc_shift_reg = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let dmc_bits_remaining = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let dmc_silence = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let dmc_output_level = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let dmc_dma_request = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let mut envelope_divider = [0u8; 3];
|
||||||
|
envelope_divider.copy_from_slice(sio::take_exact(data, &mut cursor, 3, BUS_STATE_CTX)?);
|
||||||
|
let mut envelope_decay = [0u8; 3];
|
||||||
|
envelope_decay.copy_from_slice(sio::take_exact(data, &mut cursor, 3, BUS_STATE_CTX)?);
|
||||||
|
let envelope_start_flags = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let triangle_linear_counter = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let triangle_linear_reload_flag = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let mut sweep_divider = [0u8; 2];
|
||||||
|
sweep_divider.copy_from_slice(sio::take_exact(data, &mut cursor, 2, BUS_STATE_CTX)?);
|
||||||
|
let sweep_reload_flags = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let cpu_cycle_parity = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let frame_reset_pending = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let frame_reset_delay = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)?;
|
||||||
|
let pending_frame_mode_5step = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
let pending_frame_irq_inhibit = sio::take_u8(data, &mut cursor, BUS_STATE_CTX)? != 0;
|
||||||
|
self.apu.load_state_tail(ApuStateTail {
|
||||||
|
frame_cycle,
|
||||||
|
frame_mode_5step,
|
||||||
|
frame_irq_inhibit,
|
||||||
|
frame_irq_pending,
|
||||||
|
channel_enable_mask,
|
||||||
|
length_counters,
|
||||||
|
dmc_bytes_remaining,
|
||||||
|
dmc_irq_enabled,
|
||||||
|
dmc_irq_pending,
|
||||||
|
dmc_cycle_counter,
|
||||||
|
dmc_current_addr,
|
||||||
|
dmc_sample_buffer,
|
||||||
|
dmc_sample_buffer_valid,
|
||||||
|
dmc_shift_reg,
|
||||||
|
dmc_bits_remaining,
|
||||||
|
dmc_silence,
|
||||||
|
dmc_output_level,
|
||||||
|
dmc_dma_request,
|
||||||
|
envelope_divider,
|
||||||
|
envelope_decay,
|
||||||
|
envelope_start_flags,
|
||||||
|
triangle_linear_counter,
|
||||||
|
triangle_linear_reload_flag,
|
||||||
|
sweep_divider,
|
||||||
|
sweep_reload_flags,
|
||||||
|
cpu_cycle_parity,
|
||||||
|
frame_reset_pending,
|
||||||
|
frame_reset_delay,
|
||||||
|
pending_frame_mode_5step,
|
||||||
|
pending_frame_irq_inhibit,
|
||||||
|
});
|
||||||
|
let mapper_len = sio::take_u32(data, &mut cursor, BUS_STATE_CTX)? as usize;
|
||||||
|
let mapper_state = sio::take_exact(data, &mut cursor, mapper_len, BUS_STATE_CTX)?;
|
||||||
|
|
||||||
|
self.ppu.set_vblank(self.in_vblank);
|
||||||
|
self.mapper.load_state(mapper_state)?;
|
||||||
|
|
||||||
|
if cursor != data.len() {
|
||||||
|
return Err("bus state: trailing bytes in payload".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/native_core/bus/tests.rs
Normal file
159
src/native_core/bus/tests.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use super::{
|
||||||
|
CpuBus, NativeBus, PPU_DOTS_PER_SCANLINE, PPU_PRERENDER_SCANLINE, PPU_VBLANK_START_SCANLINE,
|
||||||
|
};
|
||||||
|
use crate::native_core::{ines::Mirroring, mapper::Mapper};
|
||||||
|
|
||||||
|
struct StubMapper;
|
||||||
|
|
||||||
|
impl Mapper for StubMapper {
|
||||||
|
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||||
|
|
||||||
|
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScanlineIrqMapper {
|
||||||
|
irq_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for ScanlineIrqMapper {
|
||||||
|
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||||
|
|
||||||
|
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct A12GatedMapper {
|
||||||
|
irq_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for A12GatedMapper {
|
||||||
|
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||||
|
|
||||||
|
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct A12CountMapper {
|
||||||
|
clocks: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for A12CountMapper {
|
||||||
|
fn cpu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn ppu_read(&self, _addr: u16) -> u8 {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
self.clocks = self.clocks.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
if self.clocks == 0 {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
self.clocks -= 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, _out: &mut Vec<u8>) {}
|
||||||
|
|
||||||
|
fn load_state(&mut self, _data: &[u8]) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod apu;
|
||||||
|
mod mapper_timing;
|
||||||
|
mod ppu_open_bus;
|
||||||
314
src/native_core/bus/tests/apu.rs
Normal file
314
src/native_core/bus/tests/apu.rs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apu_frame_irq_asserts_in_4_step_mode() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||||
|
|
||||||
|
for _ in 0..14_918u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(bus.poll_irq(), "APU frame IRQ should assert in 4-step mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_4015_clears_apu_frame_irq_flag() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||||
|
|
||||||
|
for _ in 0..14_918u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = bus.read(0x4015);
|
||||||
|
assert_ne!(status & 0x40, 0, "frame IRQ bit should be set in status");
|
||||||
|
assert!(!bus.poll_irq(), "reading 4015 should clear frame IRQ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apu_frame_irq_inhibit_bit_disables_irq_and_clears_pending() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||||
|
for _ in 0..14_918u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert!(bus.poll_irq());
|
||||||
|
|
||||||
|
bus.write(0x4017, 0x40); // 4-step, IRQ inhibit
|
||||||
|
assert!(
|
||||||
|
!bus.poll_irq(),
|
||||||
|
"inhibit write should clear pending frame IRQ"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writing_4015_does_not_acknowledge_apu_frame_irq() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4017, 0x00); // 4-step, IRQ enabled
|
||||||
|
for _ in 0..14_918u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert!(bus.poll_irq(), "frame IRQ must be pending");
|
||||||
|
|
||||||
|
// Recreate pending frame IRQ and ensure $4015 write does not clear it.
|
||||||
|
for _ in 0..14_918u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
bus.write(0x4015, 0x00);
|
||||||
|
assert!(bus.poll_irq(), "writing $4015 must not clear frame IRQ");
|
||||||
|
|
||||||
|
// Reading $4015 still acknowledges frame IRQ as expected.
|
||||||
|
let _ = bus.read(0x4015);
|
||||||
|
assert!(!bus.poll_irq(), "reading $4015 should clear frame IRQ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apu_5step_mode_does_not_generate_frame_irq() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4017, 0x80); // 5-step mode
|
||||||
|
|
||||||
|
for _ in 0..20_000u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert!(!bus.poll_irq(), "5-step mode must not assert frame IRQ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apu_write_only_register_reads_return_cpu_open_bus() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4000, 0x12);
|
||||||
|
bus.write(0x0000, 0xAB);
|
||||||
|
assert_eq!(bus.read(0x0000), 0xAB);
|
||||||
|
|
||||||
|
assert_eq!(bus.read(0x4000), 0xAB);
|
||||||
|
assert_eq!(bus.read(0x400E), 0xAB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writing_4017_in_5step_mode_clocks_half_frame_after_delay() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4015, 0x01); // enable pulse1
|
||||||
|
bus.write(0x4000, 0x00); // length halt disabled
|
||||||
|
bus.write(0x4003, 0x18); // length index 3 => 2
|
||||||
|
assert_eq!(bus.apu.length_counters[0], 2);
|
||||||
|
|
||||||
|
bus.write(0x4017, 0x80); // switch to 5-step mode
|
||||||
|
assert_eq!(bus.apu.length_counters[0], 2);
|
||||||
|
for _ in 0..2u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.length_counters[0], 2);
|
||||||
|
bus.clock_cpu(1); // reset delay complete (3 CPU cycles on even phase)
|
||||||
|
assert_eq!(bus.apu.length_counters[0], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_roundtrip_preserves_apu_frame_counter_fields() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.apu.frame_cycle = 777;
|
||||||
|
bus.apu.frame_mode_5step = true;
|
||||||
|
bus.apu.frame_irq_inhibit = true;
|
||||||
|
bus.apu.frame_irq_pending = true;
|
||||||
|
bus.apu.channel_enable_mask = 0x1F;
|
||||||
|
bus.apu.length_counters = [1, 2, 3, 4];
|
||||||
|
bus.apu.dmc_bytes_remaining = 99;
|
||||||
|
bus.apu.dmc_irq_enabled = true;
|
||||||
|
bus.apu.dmc_irq_pending = true;
|
||||||
|
bus.apu.dmc_cycle_counter = 1234;
|
||||||
|
bus.apu.envelope_divider = [9, 8, 7];
|
||||||
|
bus.apu.envelope_decay = [6, 5, 4];
|
||||||
|
bus.apu.envelope_start_flags = 0x05;
|
||||||
|
bus.apu.triangle_linear_counter = 3;
|
||||||
|
bus.apu.triangle_linear_reload_flag = true;
|
||||||
|
bus.apu.sweep_divider = [11, 12];
|
||||||
|
bus.apu.sweep_reload_flags = 0x03;
|
||||||
|
bus.apu.cpu_cycle_parity = true;
|
||||||
|
bus.apu.frame_reset_pending = true;
|
||||||
|
bus.apu.frame_reset_delay = 2;
|
||||||
|
bus.apu.pending_frame_mode_5step = true;
|
||||||
|
bus.apu.pending_frame_irq_inhibit = false;
|
||||||
|
|
||||||
|
let mut raw = Vec::new();
|
||||||
|
bus.save_state(&mut raw);
|
||||||
|
|
||||||
|
let mut restored = NativeBus::new(Box::new(StubMapper));
|
||||||
|
restored.load_state(&raw).expect("state should load");
|
||||||
|
assert_eq!(restored.apu.frame_cycle, 777);
|
||||||
|
assert!(restored.apu.frame_mode_5step);
|
||||||
|
assert!(restored.apu.frame_irq_inhibit);
|
||||||
|
assert!(restored.apu.frame_irq_pending);
|
||||||
|
assert_eq!(restored.apu.channel_enable_mask, 0x1F);
|
||||||
|
assert_eq!(restored.apu.length_counters, [1, 2, 3, 4]);
|
||||||
|
assert_eq!(restored.apu.dmc_bytes_remaining, 99);
|
||||||
|
assert!(restored.apu.dmc_irq_enabled);
|
||||||
|
assert!(restored.apu.dmc_irq_pending);
|
||||||
|
assert_eq!(restored.apu.dmc_cycle_counter, 1234);
|
||||||
|
assert_eq!(restored.apu.envelope_divider, [9, 8, 7]);
|
||||||
|
assert_eq!(restored.apu.envelope_decay, [6, 5, 4]);
|
||||||
|
assert_eq!(restored.apu.envelope_start_flags, 0x05);
|
||||||
|
assert_eq!(restored.apu.triangle_linear_counter, 3);
|
||||||
|
assert!(restored.apu.triangle_linear_reload_flag);
|
||||||
|
assert_eq!(restored.apu.sweep_divider, [11, 12]);
|
||||||
|
assert_eq!(restored.apu.sweep_reload_flags, 0x03);
|
||||||
|
assert!(restored.apu.cpu_cycle_parity);
|
||||||
|
assert!(restored.apu.frame_reset_pending);
|
||||||
|
assert_eq!(restored.apu.frame_reset_delay, 2);
|
||||||
|
assert!(restored.apu.pending_frame_mode_5step);
|
||||||
|
assert!(!restored.apu.pending_frame_irq_inhibit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apu_status_reflects_length_counters_and_disable_clears_them() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4015, 0x0F); // enable pulse1/pulse2/triangle/noise
|
||||||
|
bus.write(0x4003, 0xF8); // load pulse1 length index 31
|
||||||
|
bus.write(0x4007, 0xF8); // load pulse2
|
||||||
|
bus.write(0x400B, 0xF8); // load triangle
|
||||||
|
bus.write(0x400F, 0xF8); // load noise
|
||||||
|
|
||||||
|
let status = bus.read(0x4015);
|
||||||
|
assert_eq!(status & 0x0F, 0x0F);
|
||||||
|
|
||||||
|
bus.write(0x4015, 0x00);
|
||||||
|
let status2 = bus.read(0x4015);
|
||||||
|
assert_eq!(status2 & 0x0F, 0x00);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apu_length_counter_decrements_on_half_frame_when_not_halted() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4015, 0x01); // enable pulse1
|
||||||
|
bus.write(0x4000, 0x00); // halt=0
|
||||||
|
bus.write(0x4003, 0x18); // length index 3 => value 2
|
||||||
|
|
||||||
|
assert_eq!(bus.apu.length_counters[0], 2);
|
||||||
|
for _ in 0..7_457u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.length_counters[0], 1);
|
||||||
|
for _ in 0..7_458u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.length_counters[0], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dmc_irq_raises_and_is_reported_in_4015_status() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4010, 0x8F); // IRQ enable, no loop, fastest rate
|
||||||
|
bus.write(0x4013, 0x00); // sample length = 1 byte
|
||||||
|
bus.write(0x4015, 0x10); // enable DMC
|
||||||
|
|
||||||
|
for _ in 0..54u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert!(bus.poll_irq());
|
||||||
|
|
||||||
|
let status = bus.read(0x4015);
|
||||||
|
assert_ne!(status & 0x80, 0, "DMC IRQ should be visible in status");
|
||||||
|
assert!(bus.poll_irq(), "status read must not clear DMC IRQ");
|
||||||
|
bus.write(0x4015, 0x10);
|
||||||
|
assert!(!bus.poll_irq(), "writing 4015 acknowledges DMC IRQ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quarter_frame_clocks_triangle_linear_counter() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4008, 0x05); // control=0, reload value=5
|
||||||
|
bus.write(0x400B, 0x00); // set reload flag
|
||||||
|
|
||||||
|
for _ in 0..3_729u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.triangle_linear_counter, 5);
|
||||||
|
assert!(!bus.apu.triangle_linear_reload_flag);
|
||||||
|
|
||||||
|
for _ in 0..3_728u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.triangle_linear_counter, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quarter_frame_envelope_start_reloads_decay() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4015, 0x01); // enable pulse1
|
||||||
|
bus.write(0x4000, 0x03); // envelope period=3
|
||||||
|
bus.write(0x4003, 0x00); // start envelope
|
||||||
|
assert_ne!(bus.apu.envelope_start_flags & 0x01, 0);
|
||||||
|
|
||||||
|
for _ in 0..3_729u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.envelope_decay[0], 15);
|
||||||
|
assert_eq!(bus.apu.envelope_divider[0], 3);
|
||||||
|
assert_eq!(bus.apu.envelope_start_flags & 0x01, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_half_frame_updates_pulse_timer_period() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4002, 0x00); // timer low
|
||||||
|
bus.write(0x4003, 0x02); // timer high => period 0x200
|
||||||
|
bus.write(0x4001, 0x82); // enable, period=1, negate=0, shift=2
|
||||||
|
|
||||||
|
for _ in 0..7_457u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.read(0x4002), 0x80);
|
||||||
|
assert_eq!(bus.apu.read(0x4003) & 0x07, 0x02);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_negative_pulse1_uses_ones_complement() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4002, 0x00); // period 0x200
|
||||||
|
bus.write(0x4003, 0x02);
|
||||||
|
bus.write(0x4001, 0x8A); // enable, period=1, negate=1, shift=2
|
||||||
|
|
||||||
|
for _ in 0..7_457u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
assert_eq!(bus.apu.read(0x4002), 0x7F);
|
||||||
|
assert_eq!(bus.apu.read(0x4003) & 0x07, 0x01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dmc_dma_fetches_sample_bytes_and_steals_cpu_cycles() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4010, 0x0F); // no IRQ, no loop, fastest period
|
||||||
|
bus.write(0x4012, 0x00); // sample start $C000
|
||||||
|
bus.write(0x4013, 0x00); // sample length = 1 byte
|
||||||
|
bus.write(0x4015, 0x10); // enable DMC (issues initial DMA request)
|
||||||
|
|
||||||
|
assert_eq!(bus.ppu_dot, 0);
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
|
||||||
|
// 1 CPU cycle + 4-cycle DMA steal = 5 total CPU cycles => 15 PPU dots.
|
||||||
|
assert_eq!(bus.ppu_dot, 15);
|
||||||
|
assert_eq!(bus.apu.dmc_bytes_remaining, 0);
|
||||||
|
assert_eq!(bus.apu.dmc_current_addr, 0xC001);
|
||||||
|
assert!(bus.apu.dmc_sample_buffer_valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dmc_playback_updates_output_level_from_sample_bits() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x4011, 0x20); // initial DMC DAC level
|
||||||
|
bus.write(0x4010, 0x0F); // fastest DMC rate
|
||||||
|
bus.write(0x4012, 0x00); // sample start $C000
|
||||||
|
bus.write(0x4013, 0x00); // 1-byte sample
|
||||||
|
bus.write(0x4015, 0x10); // enable DMC
|
||||||
|
|
||||||
|
// Service initial DMA request.
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
let initial = bus.apu.dmc_output_level;
|
||||||
|
|
||||||
|
// Stub mapper returns 0x00 sample byte, so each played bit drives output down by 2.
|
||||||
|
for _ in 0..600u32 {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(bus.apu.dmc_output_level < initial);
|
||||||
|
}
|
||||||
72
src/native_core/bus/tests/mapper_timing.rs
Normal file
72
src/native_core/bus/tests/mapper_timing.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prerender_scanline_still_clocks_mapper_scanline_irq() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(ScanlineIrqMapper { irq_pending: false }));
|
||||||
|
bus.write(0x2001, 0x18); // enable rendering
|
||||||
|
|
||||||
|
bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE + 259;
|
||||||
|
bus.clock_ppu_dot(); // now at dot 260
|
||||||
|
assert!(bus.poll_irq());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mmc3_class_scanline_clock_is_suppressed_when_a12_has_no_activity() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(A12GatedMapper { irq_pending: false }));
|
||||||
|
bus.write(0x2001, 0x18); // BG+sprites on
|
||||||
|
bus.write(0x2000, 0x00); // BG table $0000, sprite table $0000, 8x8 sprites
|
||||||
|
|
||||||
|
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE;
|
||||||
|
for _ in 0..120 {
|
||||||
|
bus.clock_ppu_dot();
|
||||||
|
}
|
||||||
|
assert!(!bus.poll_irq());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mmc3_class_scanline_clock_runs_when_pattern_table_uses_a12() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(A12GatedMapper { irq_pending: false }));
|
||||||
|
bus.write(0x2001, 0x18); // BG+sprites on
|
||||||
|
bus.write(0x2000, 0x10); // BG pattern table $1000
|
||||||
|
|
||||||
|
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE;
|
||||||
|
for _ in 0..120 {
|
||||||
|
bus.clock_ppu_dot();
|
||||||
|
}
|
||||||
|
assert!(bus.poll_irq());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mmc3_class_a12_filter_clocks_once_per_scanline() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(A12CountMapper { clocks: 0 }));
|
||||||
|
bus.write(0x2001, 0x08); // BG on
|
||||||
|
bus.write(0x2000, 0x10); // BG pattern table $1000
|
||||||
|
|
||||||
|
bus.ppu_dot = 30 * PPU_DOTS_PER_SCANLINE;
|
||||||
|
for _ in 0..PPU_DOTS_PER_SCANLINE {
|
||||||
|
bus.clock_ppu_dot();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut count = 0u8;
|
||||||
|
while bus.poll_irq() {
|
||||||
|
count = count.saturating_add(1);
|
||||||
|
}
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_roundtrip_preserves_mmc3_a12_timing_fields() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.mmc3_a12_prev_high = true;
|
||||||
|
bus.mmc3_a12_low_dots = 3;
|
||||||
|
bus.mmc3_last_irq_scanline = 123;
|
||||||
|
|
||||||
|
let mut raw = Vec::new();
|
||||||
|
bus.save_state(&mut raw);
|
||||||
|
|
||||||
|
let mut restored = NativeBus::new(Box::new(StubMapper));
|
||||||
|
restored.load_state(&raw).expect("state should load");
|
||||||
|
assert!(restored.mmc3_a12_prev_high);
|
||||||
|
assert_eq!(restored.mmc3_a12_low_dots, 3);
|
||||||
|
assert_eq!(restored.mmc3_last_irq_scanline, 123);
|
||||||
|
}
|
||||||
191
src/native_core/bus/tests/ppu_open_bus.rs
Normal file
191
src/native_core/bus/tests/ppu_open_bus.rs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_ppustatus_clears_latched_nmi_request() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x2000, 0x80); // enable NMI on VBlank
|
||||||
|
|
||||||
|
for _ in 0..100_000usize {
|
||||||
|
bus.clock_cpu(1);
|
||||||
|
if bus.nmi_pending {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(bus.nmi_pending, "vblank NMI should have latched");
|
||||||
|
|
||||||
|
let _ = bus.read(0x2002);
|
||||||
|
assert!(!bus.nmi_pending, "status read should clear pending NMI");
|
||||||
|
assert!(
|
||||||
|
!bus.poll_nmi(),
|
||||||
|
"CPU should not observe stale NMI after status read"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sprite_overflow_flag_is_set_during_rendering_and_cleared_prerender() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x2001, 0x18); // enable BG + sprites rendering
|
||||||
|
|
||||||
|
// Initialize OAM offscreen, then place 9 sprites on scanline 20.
|
||||||
|
bus.write(0x2003, 0x00);
|
||||||
|
for _ in 0..256usize {
|
||||||
|
bus.write(0x2004, 0xFF);
|
||||||
|
}
|
||||||
|
bus.write(0x2003, 0x00);
|
||||||
|
for _ in 0..9usize {
|
||||||
|
bus.write(0x2004, 19); // Y (sprite appears at Y+1 = 20)
|
||||||
|
bus.write(0x2004, 0); // tile
|
||||||
|
bus.write(0x2004, 0); // attr
|
||||||
|
bus.write(0x2004, 0); // X
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 256;
|
||||||
|
bus.clock_ppu_dot(); // dot 257 -> overflow evaluation
|
||||||
|
assert!(bus.ppu.sprite_overflow_set());
|
||||||
|
|
||||||
|
bus.in_vblank = true;
|
||||||
|
bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE;
|
||||||
|
bus.clock_ppu_dot(); // prerender dot 1 -> clear status flags
|
||||||
|
assert!(!bus.ppu.sprite_overflow_set());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sprite_overflow_latches_until_prerender_clear() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x2001, 0x18); // rendering enabled
|
||||||
|
|
||||||
|
// Populate first 9 sprites on scanline 20, others offscreen.
|
||||||
|
bus.write(0x2003, 0x00);
|
||||||
|
for _ in 0..256usize {
|
||||||
|
bus.write(0x2004, 0xFF);
|
||||||
|
}
|
||||||
|
bus.write(0x2003, 0x00);
|
||||||
|
for _ in 0..9usize {
|
||||||
|
bus.write(0x2004, 19); // Y => visible on scanline 20
|
||||||
|
bus.write(0x2004, 0);
|
||||||
|
bus.write(0x2004, 0);
|
||||||
|
bus.write(0x2004, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 256;
|
||||||
|
bus.clock_ppu_dot(); // scanline 20 dot 257
|
||||||
|
assert!(bus.ppu.sprite_overflow_set());
|
||||||
|
|
||||||
|
// Move to a scanline with no overflow and ensure bit stays latched.
|
||||||
|
bus.ppu_dot = 100 * PPU_DOTS_PER_SCANLINE + 256;
|
||||||
|
bus.clock_ppu_dot();
|
||||||
|
assert!(bus.ppu.sprite_overflow_set());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sprite_overflow_not_evaluated_when_sprites_disabled() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x2001, 0x08); // BG enabled, sprites disabled
|
||||||
|
|
||||||
|
bus.write(0x2003, 0x00);
|
||||||
|
for _ in 0..256usize {
|
||||||
|
bus.write(0x2004, 0xFF);
|
||||||
|
}
|
||||||
|
bus.write(0x2003, 0x00);
|
||||||
|
for _ in 0..9usize {
|
||||||
|
bus.write(0x2004, 19);
|
||||||
|
bus.write(0x2004, 0);
|
||||||
|
bus.write(0x2004, 0);
|
||||||
|
bus.write(0x2004, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 256;
|
||||||
|
bus.clock_ppu_dot();
|
||||||
|
assert!(!bus.ppu.sprite_overflow_set());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn odd_frame_skips_one_ppu_dot_when_rendering_enabled() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x2001, 0x18); // rendering enabled
|
||||||
|
|
||||||
|
let mut dots = 0u32;
|
||||||
|
while !bus.frame_complete {
|
||||||
|
bus.clock_ppu_dot();
|
||||||
|
dots += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(dots, 341 * 262); // even frame
|
||||||
|
bus.frame_complete = false;
|
||||||
|
|
||||||
|
let mut odd_frame_dots = 0u32;
|
||||||
|
while !bus.frame_complete {
|
||||||
|
bus.clock_ppu_dot();
|
||||||
|
odd_frame_dots += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(odd_frame_dots, 341 * 262 - 1); // odd frame skip
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unmapped_cpu_reads_return_open_bus_value() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
|
||||||
|
bus.write(0x0000, 0xAB); // write puts value on CPU bus
|
||||||
|
assert_eq!(bus.read(0x4018), 0xAB); // unmapped APU/test range
|
||||||
|
|
||||||
|
bus.write(0x0001, 0xCD);
|
||||||
|
assert_eq!(bus.read(0x4FFF), 0xCD); // unmapped expansion range
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn joypad_read_preserves_open_bus_upper_bits_and_sets_bit6() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.set_joypad_buttons([false, false, false, false, true, false, false, false]); // A pressed
|
||||||
|
bus.write(0x4016, 0xA1); // strobe on + seed open bus high bits
|
||||||
|
let v = bus.read(0x4016);
|
||||||
|
assert_eq!(v & 0x01, 1);
|
||||||
|
assert_eq!(v & 0x40, 0x40);
|
||||||
|
assert_eq!(v & 0xE0, 0xE0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ppustatus_read_at_vblank_edge_suppresses_vblank_for_that_frame() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x2000, 0x80); // enable NMI
|
||||||
|
|
||||||
|
// Position exactly at vblank set point (scanline 241 dot 1).
|
||||||
|
bus.ppu_dot = PPU_VBLANK_START_SCANLINE * PPU_DOTS_PER_SCANLINE + 1;
|
||||||
|
let _ = bus.read(0x2002); // status read at dot 1 suppresses vblank for this frame
|
||||||
|
bus.clock_ppu_dot(); // dot 2 on scanline 241
|
||||||
|
|
||||||
|
assert!(!bus.in_vblank);
|
||||||
|
assert!(!bus.nmi_pending);
|
||||||
|
let status = bus.read(0x2002);
|
||||||
|
assert_eq!(status & 0x80, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prerender_clears_status_flags_even_if_not_in_vblank() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.in_vblank = false;
|
||||||
|
bus.ppu.set_vblank(true);
|
||||||
|
bus.ppu.set_sprite0_hit(true);
|
||||||
|
bus.ppu.set_sprite_overflow(true);
|
||||||
|
|
||||||
|
bus.ppu_dot = PPU_PRERENDER_SCANLINE * PPU_DOTS_PER_SCANLINE;
|
||||||
|
bus.clock_ppu_dot(); // prerender dot 1
|
||||||
|
|
||||||
|
let status = bus.read(0x2002);
|
||||||
|
assert_eq!(status & 0xE0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oamdata_read_returns_ff_during_active_rendering() {
|
||||||
|
let mut bus = NativeBus::new(Box::new(StubMapper));
|
||||||
|
bus.write(0x2001, 0x18); // rendering enabled
|
||||||
|
bus.write(0x2003, 0x10);
|
||||||
|
bus.write(0x2004, 0x22);
|
||||||
|
bus.write(0x2003, 0x10);
|
||||||
|
|
||||||
|
bus.ppu_dot = 20 * PPU_DOTS_PER_SCANLINE + 100;
|
||||||
|
let v = bus.read(0x2004);
|
||||||
|
assert_eq!(v, 0xFF);
|
||||||
|
|
||||||
|
bus.ppu_dot = PPU_VBLANK_START_SCANLINE * PPU_DOTS_PER_SCANLINE + 10;
|
||||||
|
let v2 = bus.read(0x2004);
|
||||||
|
assert_eq!(v2, 0x22);
|
||||||
|
}
|
||||||
116
src/native_core/bus/timing.rs
Normal file
116
src/native_core/bus/timing.rs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
use super::{
|
||||||
|
NativeBus, PPU_DOTS_PER_FRAME, PPU_DOTS_PER_SCANLINE, PPU_PRERENDER_SCANLINE,
|
||||||
|
PPU_VBLANK_START_SCANLINE,
|
||||||
|
};
|
||||||
|
use crate::native_core::cpu::CpuBus;
|
||||||
|
use crate::native_core::mapper::Mapper;
|
||||||
|
|
||||||
|
impl NativeBus {
|
||||||
|
fn clock_one_cpu_cycle(&mut self) {
|
||||||
|
for _ in 0..3 {
|
||||||
|
self.clock_ppu_dot();
|
||||||
|
}
|
||||||
|
self.mapper.clock_cpu(1);
|
||||||
|
self.apu.clock_cpu_cycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn service_dmc_dma(&mut self, addr: u16) {
|
||||||
|
let byte = self.dma_read(addr);
|
||||||
|
self.apu.provide_dmc_dma_byte(byte);
|
||||||
|
// DMC DMA steals CPU bus cycles while APU/PPU/mapper keep ticking.
|
||||||
|
for _ in 0..4 {
|
||||||
|
self.clock_one_cpu_cycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn dma_read(&mut self, addr: u16) -> u8 {
|
||||||
|
<Self as CpuBus>::read(self, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clock_ppu_dot(&mut self) {
|
||||||
|
self.ppu_dot += 1;
|
||||||
|
if self.ppu_dot >= PPU_DOTS_PER_FRAME {
|
||||||
|
self.ppu_dot = 0;
|
||||||
|
self.frame_complete = true;
|
||||||
|
self.odd_frame = !self.odd_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scanline = self.ppu_dot / PPU_DOTS_PER_SCANLINE;
|
||||||
|
let dot = self.ppu_dot % PPU_DOTS_PER_SCANLINE;
|
||||||
|
let rendering_enabled = self.ppu.rendering_enabled();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mapper: &(dyn Mapper + Send) = &*self.mapper;
|
||||||
|
self.ppu.render_dot(mapper, scanline, dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rendering_enabled && (scanline < 240 || scanline == PPU_PRERENDER_SCANLINE) {
|
||||||
|
if self.mapper.needs_ppu_a12_clock() {
|
||||||
|
let a12_high = self.ppu.mmc3_a12_high_at(scanline, dot);
|
||||||
|
if a12_high {
|
||||||
|
if !self.mmc3_a12_prev_high
|
||||||
|
&& self.mmc3_a12_low_dots >= 8
|
||||||
|
&& self.mmc3_last_irq_scanline != scanline
|
||||||
|
{
|
||||||
|
self.mapper.clock_scanline();
|
||||||
|
self.mmc3_last_irq_scanline = scanline;
|
||||||
|
}
|
||||||
|
self.mmc3_a12_prev_high = true;
|
||||||
|
self.mmc3_a12_low_dots = 0;
|
||||||
|
} else {
|
||||||
|
self.mmc3_a12_prev_high = false;
|
||||||
|
self.mmc3_a12_low_dots = self.mmc3_a12_low_dots.saturating_add(1);
|
||||||
|
}
|
||||||
|
} else if dot == 260 {
|
||||||
|
self.mapper.clock_scanline();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.mmc3_a12_prev_high = false;
|
||||||
|
self.mmc3_a12_low_dots = self.mmc3_a12_low_dots.saturating_add(1);
|
||||||
|
}
|
||||||
|
if rendering_enabled && scanline == PPU_PRERENDER_SCANLINE && dot == 339 && self.odd_frame {
|
||||||
|
// NTSC odd frame timing: skip pre-render dot 340 when rendering is enabled.
|
||||||
|
self.ppu_dot = self.ppu_dot.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.in_vblank && scanline == PPU_VBLANK_START_SCANLINE && dot == 1 {
|
||||||
|
if self.suppress_vblank_this_frame {
|
||||||
|
self.suppress_vblank_this_frame = false;
|
||||||
|
self.in_vblank = false;
|
||||||
|
self.ppu.set_vblank(false);
|
||||||
|
} else {
|
||||||
|
self.in_vblank = true;
|
||||||
|
self.ppu.set_vblank(true);
|
||||||
|
if self.ppu.nmi_enabled() {
|
||||||
|
self.nmi_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if scanline == PPU_PRERENDER_SCANLINE && dot == 1 {
|
||||||
|
self.in_vblank = false;
|
||||||
|
self.ppu.set_vblank(false);
|
||||||
|
self.ppu.set_sprite0_hit(false);
|
||||||
|
self.ppu.set_sprite_overflow(false);
|
||||||
|
self.suppress_vblank_this_frame = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clock_cpu_cycles(&mut self, cycles: u32) {
|
||||||
|
if cycles == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut remaining = cycles;
|
||||||
|
while remaining != 0 {
|
||||||
|
self.clock_one_cpu_cycle();
|
||||||
|
while let Some(addr) = self.apu.take_dmc_dma_request() {
|
||||||
|
self.service_dmc_dma(addr);
|
||||||
|
}
|
||||||
|
remaining -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn note_scroll_write_now(&mut self) {
|
||||||
|
let scanline = (self.ppu_dot / PPU_DOTS_PER_SCANLINE) as usize;
|
||||||
|
let dot = self.ppu_dot % PPU_DOTS_PER_SCANLINE;
|
||||||
|
self.ppu.note_scroll_register_write(scanline, dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/native_core/cpu.rs
Normal file
112
src/native_core/cpu.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CpuError {
|
||||||
|
UnsupportedOpcode { opcode: u8, pc: u16 },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CpuBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8;
|
||||||
|
fn write(&mut self, addr: u16, value: u8);
|
||||||
|
fn poll_nmi(&mut self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FLAG_CARRY: u8 = 0b0000_0001;
|
||||||
|
const FLAG_ZERO: u8 = 0b0000_0010;
|
||||||
|
const FLAG_IRQ_DISABLE: u8 = 0b0000_0100;
|
||||||
|
const FLAG_DECIMAL: u8 = 0b0000_1000;
|
||||||
|
const FLAG_BREAK: u8 = 0b0001_0000;
|
||||||
|
const FLAG_UNUSED: u8 = 0b0010_0000;
|
||||||
|
const FLAG_OVERFLOW: u8 = 0b0100_0000;
|
||||||
|
const FLAG_NEGATIVE: u8 = 0b1000_0000;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Cpu6502 {
|
||||||
|
pub a: u8,
|
||||||
|
pub x: u8,
|
||||||
|
pub y: u8,
|
||||||
|
pub sp: u8,
|
||||||
|
pub pc: u16,
|
||||||
|
pub p: u8,
|
||||||
|
pub halted: bool,
|
||||||
|
pub irq_delay: bool,
|
||||||
|
pub pending_nmi: bool,
|
||||||
|
pub pending_irq: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cpu6502 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
a: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
sp: 0xFD,
|
||||||
|
pc: 0,
|
||||||
|
p: FLAG_IRQ_DISABLE | FLAG_UNUSED,
|
||||||
|
halted: false,
|
||||||
|
irq_delay: false,
|
||||||
|
pending_nmi: false,
|
||||||
|
pending_irq: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub fn reset<B: CpuBus>(&mut self, bus: &mut B) {
|
||||||
|
let lo = bus.read(0xFFFC) as u16;
|
||||||
|
let hi = bus.read(0xFFFD) as u16;
|
||||||
|
self.pc = (hi << 8) | lo;
|
||||||
|
self.sp = self.sp.wrapping_sub(3);
|
||||||
|
self.p = (self.p | FLAG_IRQ_DISABLE | FLAG_UNUSED) & !FLAG_BREAK;
|
||||||
|
self.halted = false;
|
||||||
|
self.irq_delay = false;
|
||||||
|
self.pending_nmi = false;
|
||||||
|
self.pending_irq = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step<B: CpuBus>(&mut self, bus: &mut B) -> Result<u8, CpuError> {
|
||||||
|
if self.halted {
|
||||||
|
return Ok(2);
|
||||||
|
}
|
||||||
|
if self.pending_nmi {
|
||||||
|
self.pending_nmi = false;
|
||||||
|
self.pending_irq = false;
|
||||||
|
self.service_interrupt(bus, 0xFFFA, false);
|
||||||
|
self.p |= FLAG_UNUSED;
|
||||||
|
return Ok(7);
|
||||||
|
}
|
||||||
|
let irq_delayed = self.irq_delay;
|
||||||
|
self.irq_delay = false;
|
||||||
|
if self.pending_irq && !irq_delayed && (self.p & FLAG_IRQ_DISABLE) == 0 {
|
||||||
|
self.pending_irq = false;
|
||||||
|
self.service_interrupt(bus, 0xFFFE, false);
|
||||||
|
self.p |= FLAG_UNUSED;
|
||||||
|
return Ok(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pc_before = self.pc;
|
||||||
|
let opcode = self.fetch(bus);
|
||||||
|
let cycles = self.execute_opcode(bus, opcode, pc_before)?;
|
||||||
|
|
||||||
|
// Real 6502 polls IRQ/NMI near the end of the current instruction and
|
||||||
|
// services it before the next opcode fetch.
|
||||||
|
if bus.poll_nmi() {
|
||||||
|
self.pending_nmi = true;
|
||||||
|
self.pending_irq = false;
|
||||||
|
} else if !irq_delayed && (self.p & FLAG_IRQ_DISABLE) == 0 && bus.poll_irq() {
|
||||||
|
self.pending_irq = true;
|
||||||
|
}
|
||||||
|
Ok(cycles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addressing modes, ALU helpers, stack and interrupt internals.
|
||||||
|
mod helpers;
|
||||||
|
// Opcode decoder/executor table split out from cpu.rs for readability.
|
||||||
|
mod opcodes;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
245
src/native_core/cpu/helpers.rs
Normal file
245
src/native_core/cpu/helpers.rs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn fetch<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
|
||||||
|
let value = bus.read(self.pc);
|
||||||
|
self.pc = self.pc.wrapping_add(1);
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn fetch_u16<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
let lo = self.fetch(bus) as u16;
|
||||||
|
let hi = self.fetch(bus) as u16;
|
||||||
|
(hi << 8) | lo
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_u16<B: CpuBus>(&mut self, bus: &mut B, addr: u16) -> u16 {
|
||||||
|
let lo = bus.read(addr) as u16;
|
||||||
|
let hi = bus.read(addr.wrapping_add(1)) as u16;
|
||||||
|
(hi << 8) | lo
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_u16_bug<B: CpuBus>(&mut self, bus: &mut B, addr: u16) -> u16 {
|
||||||
|
let lo = bus.read(addr) as u16;
|
||||||
|
let hi_addr = (addr & 0xFF00) | (addr.wrapping_add(1) & 0x00FF);
|
||||||
|
let hi = bus.read(hi_addr) as u16;
|
||||||
|
(hi << 8) | lo
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_zp<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
self.fetch(bus) as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_zpx<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
self.fetch(bus).wrapping_add(self.x) as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_zpy<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
self.fetch(bus).wrapping_add(self.y) as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_abs<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
self.fetch_u16(bus)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_abx<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
self.addr_abx_cross(bus).0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_aby<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
self.addr_aby_cross(bus).0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_indx<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
let ptr = self.fetch(bus).wrapping_add(self.x);
|
||||||
|
let lo = bus.read(ptr as u16) as u16;
|
||||||
|
let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
|
||||||
|
(hi << 8) | lo
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_indy<B: CpuBus>(&mut self, bus: &mut B) -> u16 {
|
||||||
|
self.addr_indy_cross(bus).0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_abx_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u16, bool) {
|
||||||
|
let base = self.fetch_u16(bus);
|
||||||
|
let addr = base.wrapping_add(self.x as u16);
|
||||||
|
(addr, (base & 0xFF00) != (addr & 0xFF00))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_aby_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u16, bool) {
|
||||||
|
let base = self.fetch_u16(bus);
|
||||||
|
let addr = base.wrapping_add(self.y as u16);
|
||||||
|
(addr, (base & 0xFF00) != (addr & 0xFF00))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn addr_indy_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u16, bool) {
|
||||||
|
let ptr = self.fetch(bus);
|
||||||
|
let lo = bus.read(ptr as u16) as u16;
|
||||||
|
let hi = bus.read(ptr.wrapping_add(1) as u16) as u16;
|
||||||
|
let base = (hi << 8) | lo;
|
||||||
|
let addr = base.wrapping_add(self.y as u16);
|
||||||
|
(addr, (base & 0xFF00) != (addr & 0xFF00))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_zp<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
|
||||||
|
let addr = self.addr_zp(bus);
|
||||||
|
bus.read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_zpx<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
|
||||||
|
let addr = self.addr_zpx(bus);
|
||||||
|
bus.read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_zpy<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
|
||||||
|
let addr = self.addr_zpy(bus);
|
||||||
|
bus.read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_abs<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
|
||||||
|
let addr = self.addr_abs(bus);
|
||||||
|
bus.read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_abx_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u8, bool) {
|
||||||
|
let (addr, crossed) = self.addr_abx_cross(bus);
|
||||||
|
(bus.read(addr), crossed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_aby_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u8, bool) {
|
||||||
|
let (addr, crossed) = self.addr_aby_cross(bus);
|
||||||
|
(bus.read(addr), crossed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_indx<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
|
||||||
|
let addr = self.addr_indx(bus);
|
||||||
|
bus.read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_indy_cross<B: CpuBus>(&mut self, bus: &mut B) -> (u8, bool) {
|
||||||
|
let (addr, crossed) = self.addr_indy_cross(bus);
|
||||||
|
(bus.read(addr), crossed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn push<B: CpuBus>(&mut self, bus: &mut B, value: u8) {
|
||||||
|
let addr = 0x0100 | self.sp as u16;
|
||||||
|
bus.write(addr, value);
|
||||||
|
self.sp = self.sp.wrapping_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn pop<B: CpuBus>(&mut self, bus: &mut B) -> u8 {
|
||||||
|
self.sp = self.sp.wrapping_add(1);
|
||||||
|
let addr = 0x0100 | self.sp as u16;
|
||||||
|
bus.read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_zn(&mut self, value: u8) {
|
||||||
|
if value == 0 {
|
||||||
|
self.p |= FLAG_ZERO;
|
||||||
|
} else {
|
||||||
|
self.p &= !FLAG_ZERO;
|
||||||
|
}
|
||||||
|
if (value & 0x80) != 0 {
|
||||||
|
self.p |= FLAG_NEGATIVE;
|
||||||
|
} else {
|
||||||
|
self.p &= !FLAG_NEGATIVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_flag(&mut self, mask: u8, enabled: bool) {
|
||||||
|
if enabled {
|
||||||
|
self.p |= mask;
|
||||||
|
} else {
|
||||||
|
self.p &= !mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn compare(&mut self, lhs: u8, rhs: u8) {
|
||||||
|
let result = lhs.wrapping_sub(rhs);
|
||||||
|
self.set_flag(FLAG_CARRY, lhs >= rhs);
|
||||||
|
self.set_zn(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn bit(&mut self, value: u8) {
|
||||||
|
self.set_flag(FLAG_ZERO, (self.a & value) == 0);
|
||||||
|
self.set_flag(FLAG_OVERFLOW, (value & 0x40) != 0);
|
||||||
|
self.set_flag(FLAG_NEGATIVE, (value & 0x80) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn adc(&mut self, value: u8) {
|
||||||
|
let carry = u16::from((self.p & FLAG_CARRY) != 0);
|
||||||
|
let a = self.a as u16;
|
||||||
|
let b = value as u16;
|
||||||
|
let sum = a + b + carry;
|
||||||
|
let result = sum as u8;
|
||||||
|
|
||||||
|
self.set_flag(FLAG_CARRY, sum > 0xFF);
|
||||||
|
let overflow = ((self.a ^ result) & (value ^ result) & 0x80) != 0;
|
||||||
|
self.set_flag(FLAG_OVERFLOW, overflow);
|
||||||
|
|
||||||
|
self.a = result;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn asl(&mut self, value: u8) -> u8 {
|
||||||
|
self.set_flag(FLAG_CARRY, (value & 0x80) != 0);
|
||||||
|
let out = value << 1;
|
||||||
|
self.set_zn(out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn lsr(&mut self, value: u8) -> u8 {
|
||||||
|
self.set_flag(FLAG_CARRY, (value & 0x01) != 0);
|
||||||
|
let out = value >> 1;
|
||||||
|
self.set_zn(out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn rol(&mut self, value: u8) -> u8 {
|
||||||
|
let carry_in = u8::from((self.p & FLAG_CARRY) != 0);
|
||||||
|
self.set_flag(FLAG_CARRY, (value & 0x80) != 0);
|
||||||
|
let out = (value << 1) | carry_in;
|
||||||
|
self.set_zn(out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ror(&mut self, value: u8) -> u8 {
|
||||||
|
let carry_in = if (self.p & FLAG_CARRY) != 0 { 0x80 } else { 0 };
|
||||||
|
self.set_flag(FLAG_CARRY, (value & 0x01) != 0);
|
||||||
|
let out = (value >> 1) | carry_in;
|
||||||
|
self.set_zn(out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn branch<B: CpuBus>(&mut self, bus: &mut B, condition: bool) -> u8 {
|
||||||
|
let offset = self.fetch(bus) as i8;
|
||||||
|
if !condition {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
let old_pc = self.pc;
|
||||||
|
self.pc = self.pc.wrapping_add_signed(offset as i16);
|
||||||
|
if (old_pc & 0xFF00) != (self.pc & 0xFF00) {
|
||||||
|
4
|
||||||
|
} else {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn service_interrupt<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
vector_addr: u16,
|
||||||
|
break_flag: bool,
|
||||||
|
) {
|
||||||
|
self.push(bus, (self.pc >> 8) as u8);
|
||||||
|
self.push(bus, self.pc as u8);
|
||||||
|
let mut status = (self.p | FLAG_UNUSED) & !FLAG_BREAK;
|
||||||
|
if break_flag {
|
||||||
|
status |= FLAG_BREAK;
|
||||||
|
}
|
||||||
|
self.push(bus, status);
|
||||||
|
self.p = (self.p | FLAG_IRQ_DISABLE | FLAG_UNUSED) & !FLAG_BREAK;
|
||||||
|
self.pc = self.read_u16(bus, vector_addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/native_core/cpu/opcodes/mod.rs
Normal file
29
src/native_core/cpu/opcodes/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod official;
|
||||||
|
mod ops;
|
||||||
|
mod undocumented;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_opcode<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
pc_before: u16,
|
||||||
|
) -> Result<u8, CpuError> {
|
||||||
|
if let Some(cycles) = self.execute_undocumented(bus, opcode, pc_before) {
|
||||||
|
self.p |= FLAG_UNUSED;
|
||||||
|
return Ok(cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cycles) = self.execute_official(bus, opcode) {
|
||||||
|
self.p |= FLAG_UNUSED;
|
||||||
|
return Ok(cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(CpuError::UnsupportedOpcode {
|
||||||
|
opcode,
|
||||||
|
pc: pc_before,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
375
src/native_core/cpu/opcodes/official/alu.rs
Normal file
375
src/native_core/cpu/opcodes/official/alu.rs
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
use super::ops::{OperandReadMode, OperandWriteMode};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_official_alu<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let cycles = match opcode {
|
||||||
|
// ADC
|
||||||
|
0x69 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x65 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x75 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x6D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x7D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x79 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x61 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x71 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.adc(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// SBC
|
||||||
|
0xE9 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xE5 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xF5 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xED => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xFD => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xF9 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xE1 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xF1 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.adc(!value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// AND
|
||||||
|
0x29 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x25 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x35 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x2D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x3D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x39 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x21 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x31 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.a &= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORA
|
||||||
|
0x09 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x05 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x15 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x0D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x19 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x1D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x01 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x11 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.a |= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// EOR
|
||||||
|
0x49 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x45 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x55 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x4D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x5D => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x59 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x41 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x51 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.a ^= value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// BIT
|
||||||
|
0x24 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.bit(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0x2C => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.bit(value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMP
|
||||||
|
0xC9 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xC5 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xD5 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xCD => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xDD => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xD9 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xC1 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xD1 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.compare(self.a, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPX / CPY
|
||||||
|
0xE0 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.compare(self.x, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xE4 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.compare(self.x, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xEC => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.compare(self.x, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xC0 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.compare(self.y, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xC4 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.compare(self.y, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xCC => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.compare(self.y, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASL
|
||||||
|
0x0A => {
|
||||||
|
self.a = self.asl(self.a);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x06 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.asl(value)),
|
||||||
|
0x16 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.asl(value)),
|
||||||
|
0x0E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.asl(value)),
|
||||||
|
0x1E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.asl(value)),
|
||||||
|
|
||||||
|
// LSR
|
||||||
|
0x4A => {
|
||||||
|
self.a = self.lsr(self.a);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x46 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.lsr(value)),
|
||||||
|
0x56 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.lsr(value)),
|
||||||
|
0x4E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.lsr(value)),
|
||||||
|
0x5E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.lsr(value)),
|
||||||
|
|
||||||
|
// ROL
|
||||||
|
0x2A => {
|
||||||
|
self.a = self.rol(self.a);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x26 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.rol(value)),
|
||||||
|
0x36 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.rol(value)),
|
||||||
|
0x2E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.rol(value)),
|
||||||
|
0x3E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.rol(value)),
|
||||||
|
|
||||||
|
// ROR
|
||||||
|
0x6A => {
|
||||||
|
self.a = self.ror(self.a);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x66 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| cpu.ror(value)),
|
||||||
|
0x76 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| cpu.ror(value)),
|
||||||
|
0x6E => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| cpu.ror(value)),
|
||||||
|
0x7E => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| cpu.ror(value)),
|
||||||
|
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(cycles)
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/native_core/cpu/opcodes/official/control.rs
Normal file
133
src/native_core/cpu/opcodes/official/control.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_official_control<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let cycles = match opcode {
|
||||||
|
0xEA => 2,
|
||||||
|
|
||||||
|
// Jumps / calls
|
||||||
|
0x4C => {
|
||||||
|
self.pc = self.addr_abs(bus);
|
||||||
|
3
|
||||||
|
}
|
||||||
|
0x6C => {
|
||||||
|
let ptr = self.addr_abs(bus);
|
||||||
|
self.pc = self.read_u16_bug(bus, ptr);
|
||||||
|
5
|
||||||
|
}
|
||||||
|
0x20 => {
|
||||||
|
let addr = self.addr_abs(bus);
|
||||||
|
let ret = self.pc.wrapping_sub(1);
|
||||||
|
self.push(bus, (ret >> 8) as u8);
|
||||||
|
self.push(bus, ret as u8);
|
||||||
|
self.pc = addr;
|
||||||
|
6
|
||||||
|
}
|
||||||
|
0x60 => {
|
||||||
|
let lo = self.pop(bus) as u16;
|
||||||
|
let hi = self.pop(bus) as u16;
|
||||||
|
self.pc = ((hi << 8) | lo).wrapping_add(1);
|
||||||
|
6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack
|
||||||
|
0x48 => {
|
||||||
|
self.push(bus, self.a);
|
||||||
|
3
|
||||||
|
}
|
||||||
|
0x68 => {
|
||||||
|
self.a = self.pop(bus);
|
||||||
|
self.set_zn(self.a);
|
||||||
|
4
|
||||||
|
}
|
||||||
|
0x08 => {
|
||||||
|
self.push(bus, self.p | FLAG_BREAK | FLAG_UNUSED);
|
||||||
|
3
|
||||||
|
}
|
||||||
|
0x28 => {
|
||||||
|
let old_i = (self.p & FLAG_IRQ_DISABLE) != 0;
|
||||||
|
self.p = (self.pop(bus) | FLAG_UNUSED) & !FLAG_BREAK;
|
||||||
|
let new_i = (self.p & FLAG_IRQ_DISABLE) != 0;
|
||||||
|
if old_i && !new_i {
|
||||||
|
self.irq_delay = true;
|
||||||
|
}
|
||||||
|
4
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
0x18 => {
|
||||||
|
self.p &= !FLAG_CARRY;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x38 => {
|
||||||
|
self.p |= FLAG_CARRY;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x58 => {
|
||||||
|
self.p &= !FLAG_IRQ_DISABLE;
|
||||||
|
self.irq_delay = true;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x78 => {
|
||||||
|
self.p |= FLAG_IRQ_DISABLE;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xD8 => {
|
||||||
|
self.p &= !FLAG_DECIMAL;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xF8 => {
|
||||||
|
self.p |= FLAG_DECIMAL;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xB8 => {
|
||||||
|
self.p &= !FLAG_OVERFLOW;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
// BRK / RTI
|
||||||
|
0x00 => {
|
||||||
|
self.pc = self.pc.wrapping_add(1);
|
||||||
|
self.push(bus, (self.pc >> 8) as u8);
|
||||||
|
self.push(bus, self.pc as u8);
|
||||||
|
self.push(bus, self.p | FLAG_BREAK | FLAG_UNUSED);
|
||||||
|
self.p |= FLAG_IRQ_DISABLE;
|
||||||
|
// NMI can hijack BRK and force vector $FFFA while preserving B=1 on stack.
|
||||||
|
let vector = if bus.poll_nmi() { 0xFFFA } else { 0xFFFE };
|
||||||
|
self.pending_nmi = false;
|
||||||
|
self.pending_irq = false;
|
||||||
|
self.pc = self.read_u16(bus, vector);
|
||||||
|
7
|
||||||
|
}
|
||||||
|
0x40 => {
|
||||||
|
let old_i = (self.p & FLAG_IRQ_DISABLE) != 0;
|
||||||
|
self.p = (self.pop(bus) | FLAG_UNUSED) & !FLAG_BREAK;
|
||||||
|
let new_i = (self.p & FLAG_IRQ_DISABLE) != 0;
|
||||||
|
if old_i && !new_i {
|
||||||
|
self.irq_delay = true;
|
||||||
|
}
|
||||||
|
let lo = self.pop(bus) as u16;
|
||||||
|
let hi = self.pop(bus) as u16;
|
||||||
|
self.pc = (hi << 8) | lo;
|
||||||
|
6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branches
|
||||||
|
0xD0 => self.branch(bus, (self.p & FLAG_ZERO) == 0),
|
||||||
|
0xF0 => self.branch(bus, (self.p & FLAG_ZERO) != 0),
|
||||||
|
0x10 => self.branch(bus, (self.p & FLAG_NEGATIVE) == 0),
|
||||||
|
0x30 => self.branch(bus, (self.p & FLAG_NEGATIVE) != 0),
|
||||||
|
0x90 => self.branch(bus, (self.p & FLAG_CARRY) == 0),
|
||||||
|
0xB0 => self.branch(bus, (self.p & FLAG_CARRY) != 0),
|
||||||
|
0x50 => self.branch(bus, (self.p & FLAG_OVERFLOW) == 0),
|
||||||
|
0x70 => self.branch(bus, (self.p & FLAG_OVERFLOW) != 0),
|
||||||
|
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(cycles)
|
||||||
|
}
|
||||||
|
}
|
||||||
239
src/native_core/cpu/opcodes/official/load_store.rs
Normal file
239
src/native_core/cpu/opcodes/official/load_store.rs
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
use super::ops::{OperandReadMode, OperandWriteMode};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_official_load_store<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let cycles = match opcode {
|
||||||
|
// LDA
|
||||||
|
0xA9 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xA5 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xB5 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xAD => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xBD => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xB9 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xA1 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xB1 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.a = value;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDX
|
||||||
|
0xA2 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.x = value;
|
||||||
|
self.set_zn(self.x);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xA6 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.x = value;
|
||||||
|
self.set_zn(self.x);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xB6 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpy, 4);
|
||||||
|
self.x = value;
|
||||||
|
self.set_zn(self.x);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xAE => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.x = value;
|
||||||
|
self.set_zn(self.x);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xBE => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.x = value;
|
||||||
|
self.set_zn(self.x);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDY
|
||||||
|
0xA0 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.y = value;
|
||||||
|
self.set_zn(self.y);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xA4 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.y = value;
|
||||||
|
self.set_zn(self.y);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xB4 => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Zpx, 4);
|
||||||
|
self.y = value;
|
||||||
|
self.set_zn(self.y);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xAC => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.y = value;
|
||||||
|
self.set_zn(self.y);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xBC => {
|
||||||
|
let (value, cycles) = self.op_read(bus, OperandReadMode::AbxCross, 4);
|
||||||
|
self.y = value;
|
||||||
|
self.set_zn(self.y);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// STA/STX/STY
|
||||||
|
0x85 => self.op_store(bus, OperandWriteMode::Zp, self.a, 3),
|
||||||
|
0x95 => self.op_store(bus, OperandWriteMode::Zpx, self.a, 4),
|
||||||
|
0x8D => self.op_store(bus, OperandWriteMode::Abs, self.a, 4),
|
||||||
|
0x9D => self.op_store(bus, OperandWriteMode::Abx, self.a, 5),
|
||||||
|
0x99 => self.op_store(bus, OperandWriteMode::Aby, self.a, 5),
|
||||||
|
0x81 => self.op_store(bus, OperandWriteMode::Indx, self.a, 6),
|
||||||
|
0x91 => self.op_store(bus, OperandWriteMode::Indy, self.a, 6),
|
||||||
|
0x86 => self.op_store(bus, OperandWriteMode::Zp, self.x, 3),
|
||||||
|
0x96 => self.op_store(bus, OperandWriteMode::Zpy, self.x, 4),
|
||||||
|
0x8E => self.op_store(bus, OperandWriteMode::Abs, self.x, 4),
|
||||||
|
0x84 => self.op_store(bus, OperandWriteMode::Zp, self.y, 3),
|
||||||
|
0x94 => self.op_store(bus, OperandWriteMode::Zpx, self.y, 4),
|
||||||
|
0x8C => self.op_store(bus, OperandWriteMode::Abs, self.y, 4),
|
||||||
|
|
||||||
|
// Transfers
|
||||||
|
0xAA => {
|
||||||
|
self.x = self.a;
|
||||||
|
self.set_zn(self.x);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x8A => {
|
||||||
|
self.a = self.x;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xA8 => {
|
||||||
|
self.y = self.a;
|
||||||
|
self.set_zn(self.y);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x98 => {
|
||||||
|
self.a = self.y;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xBA => {
|
||||||
|
self.x = self.sp;
|
||||||
|
self.set_zn(self.x);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x9A => {
|
||||||
|
self.sp = self.x;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increments / decrements regs
|
||||||
|
0xE8 => {
|
||||||
|
self.x = self.x.wrapping_add(1);
|
||||||
|
self.set_zn(self.x);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xC8 => {
|
||||||
|
self.y = self.y.wrapping_add(1);
|
||||||
|
self.set_zn(self.y);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xCA => {
|
||||||
|
self.x = self.x.wrapping_sub(1);
|
||||||
|
self.set_zn(self.x);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x88 => {
|
||||||
|
self.y = self.y.wrapping_sub(1);
|
||||||
|
self.set_zn(self.y);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
// INC/DEC memory
|
||||||
|
0xE6 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let value = value.wrapping_add(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
0xF6 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let value = value.wrapping_add(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
0xEE => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let value = value.wrapping_add(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
0xFE => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let value = value.wrapping_add(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
0xC6 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let value = value.wrapping_sub(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
0xD6 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let value = value.wrapping_sub(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
0xCE => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let value = value.wrapping_sub(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
0xDE => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let value = value.wrapping_sub(1);
|
||||||
|
cpu.set_zn(value);
|
||||||
|
value
|
||||||
|
}),
|
||||||
|
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(cycles)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/native_core/cpu/opcodes/official/mod.rs
Normal file
13
src/native_core/cpu/opcodes/official/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod alu;
|
||||||
|
mod control;
|
||||||
|
mod load_store;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_official<B: CpuBus>(&mut self, bus: &mut B, opcode: u8) -> Option<u8> {
|
||||||
|
self.execute_official_load_store(bus, opcode)
|
||||||
|
.or_else(|| self.execute_official_alu(bus, opcode))
|
||||||
|
.or_else(|| self.execute_official_control(bus, opcode))
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/native_core/cpu/opcodes/ops.rs
Normal file
100
src/native_core/cpu/opcodes/ops.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(super) enum OperandReadMode {
|
||||||
|
Imm,
|
||||||
|
Zp,
|
||||||
|
Zpx,
|
||||||
|
Zpy,
|
||||||
|
Abs,
|
||||||
|
AbxCross,
|
||||||
|
AbyCross,
|
||||||
|
Indx,
|
||||||
|
IndyCross,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(super) enum OperandWriteMode {
|
||||||
|
Zp,
|
||||||
|
Zpx,
|
||||||
|
Zpy,
|
||||||
|
Abs,
|
||||||
|
Abx,
|
||||||
|
Aby,
|
||||||
|
Indx,
|
||||||
|
Indy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn op_read<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
mode: OperandReadMode,
|
||||||
|
base_cycles: u8,
|
||||||
|
) -> (u8, u8) {
|
||||||
|
match mode {
|
||||||
|
OperandReadMode::Imm => (self.fetch(bus), base_cycles),
|
||||||
|
OperandReadMode::Zp => (self.read_zp(bus), base_cycles),
|
||||||
|
OperandReadMode::Zpx => (self.read_zpx(bus), base_cycles),
|
||||||
|
OperandReadMode::Zpy => (self.read_zpy(bus), base_cycles),
|
||||||
|
OperandReadMode::Abs => (self.read_abs(bus), base_cycles),
|
||||||
|
OperandReadMode::AbxCross => {
|
||||||
|
let (value, crossed) = self.read_abx_cross(bus);
|
||||||
|
(value, base_cycles + u8::from(crossed))
|
||||||
|
}
|
||||||
|
OperandReadMode::AbyCross => {
|
||||||
|
let (value, crossed) = self.read_aby_cross(bus);
|
||||||
|
(value, base_cycles + u8::from(crossed))
|
||||||
|
}
|
||||||
|
OperandReadMode::Indx => (self.read_indx(bus), base_cycles),
|
||||||
|
OperandReadMode::IndyCross => {
|
||||||
|
let (value, crossed) = self.read_indy_cross(bus);
|
||||||
|
(value, base_cycles + u8::from(crossed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn op_store<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
mode: OperandWriteMode,
|
||||||
|
value: u8,
|
||||||
|
cycles: u8,
|
||||||
|
) -> u8 {
|
||||||
|
let addr = self.addr_for_write(bus, mode);
|
||||||
|
bus.write(addr, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn op_rmw<B: CpuBus, F>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
mode: OperandWriteMode,
|
||||||
|
cycles: u8,
|
||||||
|
op: F,
|
||||||
|
) -> u8
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self, u8) -> u8,
|
||||||
|
{
|
||||||
|
let addr = self.addr_for_write(bus, mode);
|
||||||
|
let old = bus.read(addr);
|
||||||
|
// 6502 RMW does a dummy write of the unmodified value before the final value.
|
||||||
|
bus.write(addr, old);
|
||||||
|
let value = op(self, old);
|
||||||
|
bus.write(addr, value);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addr_for_write<B: CpuBus>(&mut self, bus: &mut B, mode: OperandWriteMode) -> u16 {
|
||||||
|
match mode {
|
||||||
|
OperandWriteMode::Zp => self.addr_zp(bus),
|
||||||
|
OperandWriteMode::Zpx => self.addr_zpx(bus),
|
||||||
|
OperandWriteMode::Zpy => self.addr_zpy(bus),
|
||||||
|
OperandWriteMode::Abs => self.addr_abs(bus),
|
||||||
|
OperandWriteMode::Abx => self.addr_abx(bus),
|
||||||
|
OperandWriteMode::Aby => self.addr_aby(bus),
|
||||||
|
OperandWriteMode::Indx => self.addr_indx(bus),
|
||||||
|
OperandWriteMode::Indy => self.addr_indy(bus),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/native_core/cpu/opcodes/undocumented/combos.rs
Normal file
94
src/native_core/cpu/opcodes/undocumented/combos.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_undocumented_combos<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let cycles = match opcode {
|
||||||
|
// Undocumented immediate combos
|
||||||
|
0x0B | 0x2B => {
|
||||||
|
self.a &= self.fetch(bus);
|
||||||
|
self.set_zn(self.a);
|
||||||
|
self.set_flag(FLAG_CARRY, (self.a & 0x80) != 0);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x4B => {
|
||||||
|
self.a &= self.fetch(bus);
|
||||||
|
self.a = self.lsr(self.a);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0x6B => {
|
||||||
|
let imm = self.fetch(bus);
|
||||||
|
self.a &= imm;
|
||||||
|
self.a = self.ror(self.a);
|
||||||
|
let bit5 = (self.a & 0x20) != 0;
|
||||||
|
let bit6 = (self.a & 0x40) != 0;
|
||||||
|
self.set_flag(FLAG_CARRY, bit6);
|
||||||
|
self.set_flag(FLAG_OVERFLOW, bit5 ^ bit6);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xCB => {
|
||||||
|
let imm = self.fetch(bus);
|
||||||
|
let ax = self.a & self.x;
|
||||||
|
let v = ax.wrapping_sub(imm);
|
||||||
|
self.x = v;
|
||||||
|
self.set_flag(FLAG_CARRY, ax >= imm);
|
||||||
|
self.set_zn(self.x);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xEB => {
|
||||||
|
let value = self.fetch(bus);
|
||||||
|
self.adc(!value);
|
||||||
|
2
|
||||||
|
}
|
||||||
|
0xBB => {
|
||||||
|
let (addr, crossed) = self.addr_aby_cross(bus);
|
||||||
|
let v = bus.read(addr) & self.sp;
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.sp = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
4 + u8::from(crossed)
|
||||||
|
}
|
||||||
|
0x93 => {
|
||||||
|
let zp = self.fetch(bus);
|
||||||
|
let lo = bus.read(zp as u16) as u16;
|
||||||
|
let hi = bus.read(zp.wrapping_add(1) as u16) as u16;
|
||||||
|
let addr = ((hi << 8) | lo).wrapping_add(self.y as u16);
|
||||||
|
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
|
||||||
|
bus.write(addr, self.a & self.x & hi_mask);
|
||||||
|
6
|
||||||
|
}
|
||||||
|
0x9F => {
|
||||||
|
let addr = self.addr_aby(bus);
|
||||||
|
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
|
||||||
|
bus.write(addr, self.a & self.x & hi_mask);
|
||||||
|
5
|
||||||
|
}
|
||||||
|
0x9B => {
|
||||||
|
let addr = self.addr_aby(bus);
|
||||||
|
self.sp = self.a & self.x;
|
||||||
|
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
|
||||||
|
bus.write(addr, self.sp & hi_mask);
|
||||||
|
5
|
||||||
|
}
|
||||||
|
0x9C => {
|
||||||
|
let addr = self.addr_abx(bus);
|
||||||
|
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
|
||||||
|
bus.write(addr, self.y & hi_mask);
|
||||||
|
5
|
||||||
|
}
|
||||||
|
0x9E => {
|
||||||
|
let addr = self.addr_aby(bus);
|
||||||
|
let hi_mask = ((addr >> 8) as u8).wrapping_add(1);
|
||||||
|
bus.write(addr, self.x & hi_mask);
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(cycles)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/native_core/cpu/opcodes/undocumented/mod.rs
Normal file
18
src/native_core/cpu/opcodes/undocumented/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod combos;
|
||||||
|
mod rmw;
|
||||||
|
mod system;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_undocumented<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
pc_before: u16,
|
||||||
|
) -> Option<u8> {
|
||||||
|
self.execute_undocumented_system(bus, opcode, pc_before)
|
||||||
|
.or_else(|| self.execute_undocumented_rmw(bus, opcode))
|
||||||
|
.or_else(|| self.execute_undocumented_combos(bus, opcode))
|
||||||
|
}
|
||||||
|
}
|
||||||
258
src/native_core/cpu/opcodes/undocumented/rmw.rs
Normal file
258
src/native_core/cpu/opcodes/undocumented/rmw.rs
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
use super::ops::OperandWriteMode;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_undocumented_rmw<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let cycles = match opcode {
|
||||||
|
// Undocumented SLO: ASL M then ORA M
|
||||||
|
0x03 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
|
||||||
|
let v = cpu.asl(value);
|
||||||
|
cpu.a |= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x07 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let v = cpu.asl(value);
|
||||||
|
cpu.a |= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x0F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let v = cpu.asl(value);
|
||||||
|
cpu.a |= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x13 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
|
||||||
|
let v = cpu.asl(value);
|
||||||
|
cpu.a |= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x17 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let v = cpu.asl(value);
|
||||||
|
cpu.a |= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x1B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
|
||||||
|
let v = cpu.asl(value);
|
||||||
|
cpu.a |= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x1F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let v = cpu.asl(value);
|
||||||
|
cpu.a |= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Undocumented RLA: ROL M then AND M
|
||||||
|
0x23 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
|
||||||
|
let v = cpu.rol(value);
|
||||||
|
cpu.a &= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x27 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let v = cpu.rol(value);
|
||||||
|
cpu.a &= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x2F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let v = cpu.rol(value);
|
||||||
|
cpu.a &= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x33 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
|
||||||
|
let v = cpu.rol(value);
|
||||||
|
cpu.a &= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x37 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let v = cpu.rol(value);
|
||||||
|
cpu.a &= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x3B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
|
||||||
|
let v = cpu.rol(value);
|
||||||
|
cpu.a &= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x3F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let v = cpu.rol(value);
|
||||||
|
cpu.a &= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Undocumented SRE: LSR M then EOR M
|
||||||
|
0x43 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
|
||||||
|
let v = cpu.lsr(value);
|
||||||
|
cpu.a ^= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x47 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let v = cpu.lsr(value);
|
||||||
|
cpu.a ^= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x4F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let v = cpu.lsr(value);
|
||||||
|
cpu.a ^= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x53 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
|
||||||
|
let v = cpu.lsr(value);
|
||||||
|
cpu.a ^= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x57 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let v = cpu.lsr(value);
|
||||||
|
cpu.a ^= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x5B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
|
||||||
|
let v = cpu.lsr(value);
|
||||||
|
cpu.a ^= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x5F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let v = cpu.lsr(value);
|
||||||
|
cpu.a ^= v;
|
||||||
|
cpu.set_zn(cpu.a);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Undocumented RRA: ROR M then ADC M
|
||||||
|
0x63 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
|
||||||
|
let v = cpu.ror(value);
|
||||||
|
cpu.adc(v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x67 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let v = cpu.ror(value);
|
||||||
|
cpu.adc(v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x6F => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let v = cpu.ror(value);
|
||||||
|
cpu.adc(v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x73 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
|
||||||
|
let v = cpu.ror(value);
|
||||||
|
cpu.adc(v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x77 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let v = cpu.ror(value);
|
||||||
|
cpu.adc(v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x7B => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
|
||||||
|
let v = cpu.ror(value);
|
||||||
|
cpu.adc(v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0x7F => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let v = cpu.ror(value);
|
||||||
|
cpu.adc(v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Undocumented DCP: DEC M then CMP M
|
||||||
|
0xC3 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
|
||||||
|
let v = value.wrapping_sub(1);
|
||||||
|
cpu.compare(cpu.a, v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xC7 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let v = value.wrapping_sub(1);
|
||||||
|
cpu.compare(cpu.a, v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xCF => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let v = value.wrapping_sub(1);
|
||||||
|
cpu.compare(cpu.a, v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xD3 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
|
||||||
|
let v = value.wrapping_sub(1);
|
||||||
|
cpu.compare(cpu.a, v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xD7 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let v = value.wrapping_sub(1);
|
||||||
|
cpu.compare(cpu.a, v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xDB => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
|
||||||
|
let v = value.wrapping_sub(1);
|
||||||
|
cpu.compare(cpu.a, v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xDF => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let v = value.wrapping_sub(1);
|
||||||
|
cpu.compare(cpu.a, v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Undocumented ISC/ISB: INC M then SBC M
|
||||||
|
0xE3 => self.op_rmw(bus, OperandWriteMode::Indx, 8, |cpu, value| {
|
||||||
|
let v = value.wrapping_add(1);
|
||||||
|
cpu.adc(!v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xE7 => self.op_rmw(bus, OperandWriteMode::Zp, 5, |cpu, value| {
|
||||||
|
let v = value.wrapping_add(1);
|
||||||
|
cpu.adc(!v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xEF => self.op_rmw(bus, OperandWriteMode::Abs, 6, |cpu, value| {
|
||||||
|
let v = value.wrapping_add(1);
|
||||||
|
cpu.adc(!v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xF3 => self.op_rmw(bus, OperandWriteMode::Indy, 8, |cpu, value| {
|
||||||
|
let v = value.wrapping_add(1);
|
||||||
|
cpu.adc(!v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xF7 => self.op_rmw(bus, OperandWriteMode::Zpx, 6, |cpu, value| {
|
||||||
|
let v = value.wrapping_add(1);
|
||||||
|
cpu.adc(!v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xFB => self.op_rmw(bus, OperandWriteMode::Aby, 7, |cpu, value| {
|
||||||
|
let v = value.wrapping_add(1);
|
||||||
|
cpu.adc(!v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
0xFF => self.op_rmw(bus, OperandWriteMode::Abx, 7, |cpu, value| {
|
||||||
|
let v = value.wrapping_add(1);
|
||||||
|
cpu.adc(!v);
|
||||||
|
v
|
||||||
|
}),
|
||||||
|
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(cycles)
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/native_core/cpu/opcodes/undocumented/system.rs
Normal file
97
src/native_core/cpu/opcodes/undocumented/system.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
use super::ops::{OperandReadMode, OperandWriteMode};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Cpu6502 {
|
||||||
|
pub(super) fn execute_undocumented_system<B: CpuBus>(
|
||||||
|
&mut self,
|
||||||
|
bus: &mut B,
|
||||||
|
opcode: u8,
|
||||||
|
pc_before: u16,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let cycles = match opcode {
|
||||||
|
// Undocumented NOPs used by commercial ROMs.
|
||||||
|
0x1A | 0x3A | 0x5A | 0x7A | 0xDA | 0xFA => 2,
|
||||||
|
0x80 | 0x82 | 0x89 | 0xC2 | 0xE2 => self.op_read(bus, OperandReadMode::Imm, 2).1,
|
||||||
|
0x04 | 0x44 | 0x64 => self.op_read(bus, OperandReadMode::Zp, 3).1,
|
||||||
|
0x14 | 0x34 | 0x54 | 0x74 | 0xD4 | 0xF4 => self.op_read(bus, OperandReadMode::Zpx, 4).1,
|
||||||
|
0x0C => self.op_read(bus, OperandReadMode::Abs, 4).1,
|
||||||
|
0x1C | 0x3C | 0x5C | 0x7C | 0xDC | 0xFC => {
|
||||||
|
self.op_read(bus, OperandReadMode::AbxCross, 4).1
|
||||||
|
}
|
||||||
|
|
||||||
|
// JAM/KIL opcodes lock the real CPU until reset.
|
||||||
|
0x02 | 0x12 | 0x22 | 0x32 | 0x42 | 0x52 | 0x62 | 0x72 | 0x92 | 0xB2 | 0xD2 | 0xF2 => {
|
||||||
|
self.halted = true;
|
||||||
|
self.pc = pc_before;
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undocumented LAX: A <- M, X <- A
|
||||||
|
0xAB => {
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xA3 => {
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::Indx, 6);
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xA7 => {
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::Zp, 3);
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xAF => {
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::Abs, 4);
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xB3 => {
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::IndyCross, 5);
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xB7 => {
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::Zpy, 4);
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
0xBF => {
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::AbyCross, 4);
|
||||||
|
self.a = v;
|
||||||
|
self.x = v;
|
||||||
|
self.set_zn(v);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undocumented SAX: M <- A & X
|
||||||
|
0x83 => self.op_store(bus, OperandWriteMode::Indx, self.a & self.x, 6),
|
||||||
|
0x87 => self.op_store(bus, OperandWriteMode::Zp, self.a & self.x, 3),
|
||||||
|
0x8F => self.op_store(bus, OperandWriteMode::Abs, self.a & self.x, 4),
|
||||||
|
0x97 => self.op_store(bus, OperandWriteMode::Zpy, self.a & self.x, 4),
|
||||||
|
0x8B => {
|
||||||
|
// XAA/ANE (unstable on hardware), practical emulation form.
|
||||||
|
let (v, cycles) = self.op_read(bus, OperandReadMode::Imm, 2);
|
||||||
|
self.a = self.x & v;
|
||||||
|
self.set_zn(self.a);
|
||||||
|
cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(cycles)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/native_core/cpu/tests.rs
Normal file
10
src/native_core/cpu/tests.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use super::{Cpu6502, CpuBus, FLAG_CARRY, FLAG_NEGATIVE, FLAG_ZERO};
|
||||||
|
use crate::native_core::test_support::{TestRamBus, cpu_setup_with_reset};
|
||||||
|
|
||||||
|
fn setup_cpu_with_reset(prog: &[u8]) -> (Cpu6502, TestRamBus) {
|
||||||
|
cpu_setup_with_reset(prog)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod core;
|
||||||
|
mod interrupts;
|
||||||
|
mod property;
|
||||||
272
src/native_core/cpu/tests/core.rs
Normal file
272
src/native_core/cpu/tests/core.rs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lda_sta_and_branch_work() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xA9, 0x42, // LDA #$42
|
||||||
|
0x8D, 0x34, 0x12, // STA $1234
|
||||||
|
0xC9, 0x42, // CMP #$42
|
||||||
|
0xF0, 0x02, // BEQ +2
|
||||||
|
0xA9, 0x00, // skipped
|
||||||
|
0xA9, 0x99, // LDA #$99
|
||||||
|
]);
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
cpu.step(&mut bus).expect("opcode must be supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(bus.0[0x1234], 0x42);
|
||||||
|
assert_eq!(cpu.a, 0x99);
|
||||||
|
assert_eq!(cpu.p & FLAG_ZERO, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn branch_not_taken_uses_two_cycles() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0x38, // SEC
|
||||||
|
0x90, 0x7F, // BCC +$7F (not taken)
|
||||||
|
]);
|
||||||
|
assert_eq!(cpu.step(&mut bus).expect("SEC"), 2);
|
||||||
|
let cycles = cpu.step(&mut bus).expect("BCC");
|
||||||
|
assert_eq!(cycles, 2);
|
||||||
|
assert_eq!(cpu.pc, 0x8003);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn branch_taken_same_page_uses_three_cycles() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0x18, // CLC
|
||||||
|
0x90, 0x02, // BCC +2
|
||||||
|
0xEA, // skipped
|
||||||
|
0xEA, // target
|
||||||
|
]);
|
||||||
|
assert_eq!(cpu.step(&mut bus).expect("CLC"), 2);
|
||||||
|
let cycles = cpu.step(&mut bus).expect("BCC");
|
||||||
|
assert_eq!(cycles, 3);
|
||||||
|
assert_eq!(cpu.pc, 0x8005);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn branch_taken_cross_page_uses_four_cycles() {
|
||||||
|
let (_cpu, mut bus) = setup_cpu_with_reset(&[]);
|
||||||
|
bus.0[0x80FD] = 0x90; // BCC
|
||||||
|
bus.0[0x80FE] = 0x02; // target $8101 (page-crossing from $80FF)
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502 {
|
||||||
|
pc: 0x80FD,
|
||||||
|
..Cpu6502::default()
|
||||||
|
};
|
||||||
|
cpu.p &= !FLAG_CARRY;
|
||||||
|
let cycles = cpu.step(&mut bus).expect("BCC cross-page");
|
||||||
|
assert_eq!(cycles, 4);
|
||||||
|
assert_eq!(cpu.pc, 0x8101);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jsr_rts_roundtrip() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0x20, 0x06, 0x80, // JSR $8006
|
||||||
|
0xA9, 0x11, // LDA #$11
|
||||||
|
0xEA, // NOP
|
||||||
|
0xA9, 0x77, // LDA #$77
|
||||||
|
0x60, // RTS
|
||||||
|
]);
|
||||||
|
|
||||||
|
for _ in 0..4 {
|
||||||
|
cpu.step(&mut bus).expect("opcode must be supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(cpu.a, 0x11);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adc_and_sbc_update_flags() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xA9, 0x01, // LDA #$01
|
||||||
|
0x18, // CLC
|
||||||
|
0x69, 0x01, // ADC #$01
|
||||||
|
0x38, // SEC
|
||||||
|
0xE9, 0x02, // SBC #$02
|
||||||
|
]);
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
cpu.step(&mut bus).expect("opcode must be supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(cpu.a, 0x00);
|
||||||
|
assert_ne!(cpu.p & FLAG_CARRY, 0);
|
||||||
|
assert_ne!(cpu.p & FLAG_ZERO, 0);
|
||||||
|
assert_eq!(cpu.p & FLAG_NEGATIVE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indirect_x_and_indirect_y_addressing() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xA2, 0x04, // LDX #$04
|
||||||
|
0xA1, 0x10, // LDA ($10,X) -> ptr at $14/$15
|
||||||
|
0xA0, 0x01, // LDY #$01
|
||||||
|
0xB1, 0x20, // LDA ($20),Y
|
||||||
|
]);
|
||||||
|
bus.0[0x0014] = 0x00;
|
||||||
|
bus.0[0x0015] = 0x90;
|
||||||
|
bus.0[0x9000] = 0x55;
|
||||||
|
bus.0[0x0020] = 0x10;
|
||||||
|
bus.0[0x0021] = 0x90;
|
||||||
|
bus.0[0x9011] = 0xAA;
|
||||||
|
|
||||||
|
cpu.step(&mut bus).expect("ldx");
|
||||||
|
cpu.step(&mut bus).expect("lda (ind,x)");
|
||||||
|
assert_eq!(cpu.a, 0x55);
|
||||||
|
cpu.step(&mut bus).expect("ldy");
|
||||||
|
cpu.step(&mut bus).expect("lda (ind),y");
|
||||||
|
assert_eq!(cpu.a, 0xAA);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bit_and_shift_ops_work() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xA9, 0x40, // LDA #$40
|
||||||
|
0x24, 0x10, // BIT $10
|
||||||
|
0x0A, // ASL A (0x80)
|
||||||
|
0x4A, // LSR A (0x40)
|
||||||
|
0x38, // SEC
|
||||||
|
0x6A, // ROR A (0xA0)
|
||||||
|
]);
|
||||||
|
bus.0[0x0010] = 0xC0;
|
||||||
|
|
||||||
|
for _ in 0..6 {
|
||||||
|
cpu.step(&mut bus).expect("opcode must be supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(cpu.a, 0xA0);
|
||||||
|
assert_ne!(cpu.p & FLAG_NEGATIVE, 0);
|
||||||
|
assert_eq!(cpu.p & FLAG_ZERO, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rmw_instructions_perform_dummy_write_before_final_write() {
|
||||||
|
struct TraceBus {
|
||||||
|
ram: [u8; 0x10000],
|
||||||
|
writes: Vec<(u16, u8)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuBus for TraceBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
self.ram[addr as usize]
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.ram[addr as usize] = value;
|
||||||
|
self.writes.push((addr, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bus = TraceBus {
|
||||||
|
ram: [0; 0x10000],
|
||||||
|
writes: Vec::new(),
|
||||||
|
};
|
||||||
|
bus.ram[0xFFFC] = 0x00;
|
||||||
|
bus.ram[0xFFFD] = 0x80;
|
||||||
|
bus.ram[0x8000] = 0xEE; // INC $2000
|
||||||
|
bus.ram[0x8001] = 0x00;
|
||||||
|
bus.ram[0x8002] = 0x20;
|
||||||
|
bus.ram[0x2000] = 0x7F;
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
cpu.step(&mut bus).expect("INC should execute");
|
||||||
|
|
||||||
|
let writes_to_target: Vec<_> = bus
|
||||||
|
.writes
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|(addr, _)| *addr == 0x2000)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(writes_to_target, vec![(0x2000, 0x7F), (0x2000, 0x80)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nmi_interrupt_jumps_to_vector() {
|
||||||
|
struct NmiBus {
|
||||||
|
ram: [u8; 0x10000],
|
||||||
|
nmi_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuBus for NmiBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
self.ram[addr as usize]
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.ram[addr as usize] = value;
|
||||||
|
}
|
||||||
|
fn poll_nmi(&mut self) -> bool {
|
||||||
|
let out = self.nmi_pending;
|
||||||
|
self.nmi_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bus = NmiBus {
|
||||||
|
ram: [0; 0x10000],
|
||||||
|
nmi_pending: true,
|
||||||
|
};
|
||||||
|
bus.ram[0xFFFC] = 0x00;
|
||||||
|
bus.ram[0xFFFD] = 0x80;
|
||||||
|
bus.ram[0xFFFA] = 0x34;
|
||||||
|
bus.ram[0xFFFB] = 0x12;
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
let cycles = cpu.step(&mut bus).expect("nmi should be serviced");
|
||||||
|
assert_eq!(cycles, 7);
|
||||||
|
assert_eq!(cpu.pc, 0x1234);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undocumented_nop_da_is_accepted() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xDA, // undocumented NOP
|
||||||
|
0xA9, 0x42, // LDA #$42
|
||||||
|
]);
|
||||||
|
cpu.step(&mut bus).expect("0xDA should be supported");
|
||||||
|
cpu.step(&mut bus).expect("LDA should execute after 0xDA");
|
||||||
|
assert_eq!(cpu.a, 0x42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undocumented_slo_03_is_accepted() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xA2, 0x00, // LDX #$00
|
||||||
|
0x03, 0x10, // SLO ($10,X)
|
||||||
|
]);
|
||||||
|
bus.0[0x0010] = 0x00;
|
||||||
|
bus.0[0x0011] = 0x90;
|
||||||
|
bus.0[0x9000] = 0x40;
|
||||||
|
|
||||||
|
cpu.step(&mut bus).expect("LDX");
|
||||||
|
cpu.step(&mut bus).expect("0x03 should be supported");
|
||||||
|
|
||||||
|
assert_eq!(bus.0[0x9000], 0x80);
|
||||||
|
assert_eq!(cpu.a, 0x80);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jam_opcode_halts_cpu_until_reset() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0x02, // JAM/KIL
|
||||||
|
0xA9, 0x77, // LDA #$77 (must not execute while halted)
|
||||||
|
]);
|
||||||
|
|
||||||
|
let cycles = cpu.step(&mut bus).expect("jam opcode should decode");
|
||||||
|
assert_eq!(cycles, 2);
|
||||||
|
assert!(cpu.halted);
|
||||||
|
assert_eq!(cpu.pc, 0x8000);
|
||||||
|
|
||||||
|
let cycles2 = cpu.step(&mut bus).expect("halted cpu step should be valid");
|
||||||
|
assert_eq!(cycles2, 2);
|
||||||
|
assert_eq!(cpu.pc, 0x8000);
|
||||||
|
assert_eq!(cpu.a, 0x00);
|
||||||
|
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
assert!(!cpu.halted);
|
||||||
|
assert_eq!(cpu.pc, 0x8000);
|
||||||
|
}
|
||||||
242
src/native_core/cpu/tests/interrupts.rs
Normal file
242
src/native_core/cpu/tests/interrupts.rs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_delays_irq_acceptance_by_one_instruction() {
|
||||||
|
struct IrqBus {
|
||||||
|
ram: [u8; 0x10000],
|
||||||
|
irq_level: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuBus for IrqBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
self.ram[addr as usize]
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.ram[addr as usize] = value;
|
||||||
|
}
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
self.irq_level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bus = IrqBus {
|
||||||
|
ram: [0; 0x10000],
|
||||||
|
irq_level: true,
|
||||||
|
};
|
||||||
|
bus.ram[0xFFFC] = 0x00;
|
||||||
|
bus.ram[0xFFFD] = 0x80;
|
||||||
|
bus.ram[0xFFFE] = 0x00;
|
||||||
|
bus.ram[0xFFFF] = 0x90; // IRQ vector -> $9000
|
||||||
|
bus.ram[0x8000] = 0x58; // CLI
|
||||||
|
bus.ram[0x8001] = 0xEA; // NOP
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
|
||||||
|
let c1 = cpu.step(&mut bus).expect("CLI");
|
||||||
|
assert_eq!(c1, 2);
|
||||||
|
assert_eq!(cpu.pc, 0x8001);
|
||||||
|
|
||||||
|
let c2 = cpu.step(&mut bus).expect("NOP executes before IRQ");
|
||||||
|
assert_eq!(c2, 2);
|
||||||
|
assert_eq!(cpu.pc, 0x8002);
|
||||||
|
|
||||||
|
let c3 = cpu.step(&mut bus).expect("IRQ taken now");
|
||||||
|
assert_eq!(c3, 7);
|
||||||
|
assert_eq!(cpu.pc, 0x9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plp_delays_irq_acceptance_by_one_instruction_when_clearing_i() {
|
||||||
|
struct IrqBus {
|
||||||
|
ram: [u8; 0x10000],
|
||||||
|
irq_level: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuBus for IrqBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
self.ram[addr as usize]
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.ram[addr as usize] = value;
|
||||||
|
}
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
self.irq_level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bus = IrqBus {
|
||||||
|
ram: [0; 0x10000],
|
||||||
|
irq_level: true,
|
||||||
|
};
|
||||||
|
bus.ram[0xFFFC] = 0x00;
|
||||||
|
bus.ram[0xFFFD] = 0x80;
|
||||||
|
bus.ram[0xFFFE] = 0x00;
|
||||||
|
bus.ram[0xFFFF] = 0x90;
|
||||||
|
bus.ram[0x8000] = 0xA9; // LDA #$20
|
||||||
|
bus.ram[0x8001] = 0x20;
|
||||||
|
bus.ram[0x8002] = 0x48; // PHA
|
||||||
|
bus.ram[0x8003] = 0x28; // PLP -> I clears
|
||||||
|
bus.ram[0x8004] = 0xEA; // NOP
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
|
||||||
|
cpu.step(&mut bus).expect("LDA");
|
||||||
|
cpu.step(&mut bus).expect("PHA");
|
||||||
|
let c3 = cpu.step(&mut bus).expect("PLP");
|
||||||
|
assert_eq!(c3, 4);
|
||||||
|
assert_eq!(cpu.pc, 0x8004);
|
||||||
|
|
||||||
|
let c4 = cpu.step(&mut bus).expect("NOP executes before IRQ");
|
||||||
|
assert_eq!(c4, 2);
|
||||||
|
assert_eq!(cpu.pc, 0x8005);
|
||||||
|
|
||||||
|
let c5 = cpu.step(&mut bus).expect("IRQ taken");
|
||||||
|
assert_eq!(c5, 7);
|
||||||
|
assert_eq!(cpu.pc, 0x9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jam_halt_ignores_nmi_and_irq() {
|
||||||
|
struct InterruptBus {
|
||||||
|
ram: [u8; 0x10000],
|
||||||
|
nmi: bool,
|
||||||
|
irq: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuBus for InterruptBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
self.ram[addr as usize]
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.ram[addr as usize] = value;
|
||||||
|
}
|
||||||
|
fn poll_nmi(&mut self) -> bool {
|
||||||
|
let out = self.nmi;
|
||||||
|
self.nmi = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
self.irq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bus = InterruptBus {
|
||||||
|
ram: [0; 0x10000],
|
||||||
|
nmi: false,
|
||||||
|
irq: false,
|
||||||
|
};
|
||||||
|
bus.ram[0xFFFC] = 0x00;
|
||||||
|
bus.ram[0xFFFD] = 0x80;
|
||||||
|
bus.ram[0xFFFA] = 0x00;
|
||||||
|
bus.ram[0xFFFB] = 0x90;
|
||||||
|
bus.ram[0xFFFE] = 0x00;
|
||||||
|
bus.ram[0xFFFF] = 0xA0;
|
||||||
|
bus.ram[0x8000] = 0x02; // JAM
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
cpu.step(&mut bus).expect("jam");
|
||||||
|
assert!(cpu.halted);
|
||||||
|
|
||||||
|
bus.nmi = true;
|
||||||
|
bus.irq = true;
|
||||||
|
let c = cpu.step(&mut bus).expect("halted step");
|
||||||
|
assert_eq!(c, 2);
|
||||||
|
assert_eq!(cpu.pc, 0x8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rti_delays_irq_acceptance_by_one_instruction_when_clearing_i() {
|
||||||
|
struct IrqBus {
|
||||||
|
ram: [u8; 0x10000],
|
||||||
|
irq_level: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuBus for IrqBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
self.ram[addr as usize]
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.ram[addr as usize] = value;
|
||||||
|
}
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
self.irq_level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bus = IrqBus {
|
||||||
|
ram: [0; 0x10000],
|
||||||
|
irq_level: true,
|
||||||
|
};
|
||||||
|
bus.ram[0xFFFC] = 0x00;
|
||||||
|
bus.ram[0xFFFD] = 0x80;
|
||||||
|
bus.ram[0xFFFE] = 0x00;
|
||||||
|
bus.ram[0xFFFF] = 0x90; // IRQ vector
|
||||||
|
|
||||||
|
bus.ram[0x8000] = 0x40; // RTI
|
||||||
|
bus.ram[0x8001] = 0xEA; // NOP
|
||||||
|
|
||||||
|
// After reset SP is $FA, so RTI pops from $01FB/$01FC/$01FD.
|
||||||
|
bus.ram[0x01FB] = 0x20; // P (I clear)
|
||||||
|
bus.ram[0x01FC] = 0x01; // PC low
|
||||||
|
bus.ram[0x01FD] = 0x80; // PC high
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
|
||||||
|
let c1 = cpu.step(&mut bus).expect("RTI");
|
||||||
|
assert_eq!(c1, 6);
|
||||||
|
assert_eq!(cpu.pc, 0x8001);
|
||||||
|
|
||||||
|
let c2 = cpu.step(&mut bus).expect("NOP before IRQ");
|
||||||
|
assert_eq!(c2, 2);
|
||||||
|
assert_eq!(cpu.pc, 0x8002);
|
||||||
|
|
||||||
|
let c3 = cpu.step(&mut bus).expect("IRQ now taken");
|
||||||
|
assert_eq!(c3, 7);
|
||||||
|
assert_eq!(cpu.pc, 0x9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brk_is_hijacked_by_nmi_vector_when_nmi_arrives_during_brk() {
|
||||||
|
struct NmiBus {
|
||||||
|
ram: [u8; 0x10000],
|
||||||
|
nmi_now: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuBus for NmiBus {
|
||||||
|
fn read(&mut self, addr: u16) -> u8 {
|
||||||
|
self.ram[addr as usize]
|
||||||
|
}
|
||||||
|
fn write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.ram[addr as usize] = value;
|
||||||
|
}
|
||||||
|
fn poll_nmi(&mut self) -> bool {
|
||||||
|
let out = self.nmi_now;
|
||||||
|
self.nmi_now = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bus = NmiBus {
|
||||||
|
ram: [0; 0x10000],
|
||||||
|
nmi_now: false,
|
||||||
|
};
|
||||||
|
bus.ram[0xFFFC] = 0x00;
|
||||||
|
bus.ram[0xFFFD] = 0x80;
|
||||||
|
bus.ram[0xFFFA] = 0x34; // NMI vector
|
||||||
|
bus.ram[0xFFFB] = 0x12;
|
||||||
|
bus.ram[0xFFFE] = 0x78; // IRQ/BRK vector
|
||||||
|
bus.ram[0xFFFF] = 0x56;
|
||||||
|
bus.ram[0x8000] = 0x00; // BRK
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
|
||||||
|
bus.nmi_now = true;
|
||||||
|
let cycles = cpu.step(&mut bus).expect("BRK");
|
||||||
|
assert_eq!(cycles, 7);
|
||||||
|
assert_eq!(cpu.pc, 0x1234, "NMI vector should hijack BRK");
|
||||||
|
}
|
||||||
56
src/native_core/cpu/tests/property.rs
Normal file
56
src/native_core/cpu/tests/property.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn property_all_256_opcodes_decode_without_error() {
|
||||||
|
for opcode in 0u16..=255 {
|
||||||
|
let mut bus = TestRamBus::new();
|
||||||
|
bus.0[0xFFFC] = 0x00;
|
||||||
|
bus.0[0xFFFD] = 0x80;
|
||||||
|
bus.0[0xFFFE] = 0x00;
|
||||||
|
bus.0[0xFFFF] = 0x80;
|
||||||
|
bus.0[0xFFFA] = 0x00;
|
||||||
|
bus.0[0xFFFB] = 0x80;
|
||||||
|
bus.0[0x8000] = opcode as u8;
|
||||||
|
bus.0[0x8001] = 0x00;
|
||||||
|
bus.0[0x8002] = 0x00;
|
||||||
|
|
||||||
|
let mut cpu = Cpu6502::default();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
let res = cpu.step(&mut bus);
|
||||||
|
assert!(res.is_ok(), "opcode {:02X} must be supported", opcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn property_lda_abs_x_adds_cycle_on_page_cross() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xA2, 0x01, // LDX #$01
|
||||||
|
0xBD, 0xFF, 0x12, // LDA $12FF,X -> crosses to $1300
|
||||||
|
]);
|
||||||
|
bus.0[0x1300] = 0x77;
|
||||||
|
|
||||||
|
let c1 = cpu.step(&mut bus).expect("LDX");
|
||||||
|
let c2 = cpu.step(&mut bus).expect("LDA abs,X");
|
||||||
|
|
||||||
|
assert_eq!(c1, 2);
|
||||||
|
assert_eq!(c2, 5);
|
||||||
|
assert_eq!(cpu.a, 0x77);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn property_lda_ind_y_adds_cycle_on_page_cross() {
|
||||||
|
let (mut cpu, mut bus) = setup_cpu_with_reset(&[
|
||||||
|
0xA0, 0x01, // LDY #$01
|
||||||
|
0xB1, 0x10, // LDA ($10),Y
|
||||||
|
]);
|
||||||
|
bus.0[0x0010] = 0xFF;
|
||||||
|
bus.0[0x0011] = 0x12;
|
||||||
|
bus.0[0x1300] = 0x55;
|
||||||
|
|
||||||
|
let c1 = cpu.step(&mut bus).expect("LDY");
|
||||||
|
let c2 = cpu.step(&mut bus).expect("LDA ind,Y");
|
||||||
|
|
||||||
|
assert_eq!(c1, 2);
|
||||||
|
assert_eq!(c2, 6);
|
||||||
|
assert_eq!(cpu.a, 0x55);
|
||||||
|
}
|
||||||
225
src/native_core/ines.rs
Normal file
225
src/native_core/ines.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
const INES_HEADER_LEN: usize = 16;
|
||||||
|
const TRAINER_LEN: usize = 512;
|
||||||
|
const PRG_BANK_16K: usize = 16 * 1024;
|
||||||
|
const CHR_BANK_8K: usize = 8 * 1024;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Mirroring {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
FourScreen,
|
||||||
|
OneScreenLow,
|
||||||
|
OneScreenHigh,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct InesHeader {
|
||||||
|
pub mapper: u16,
|
||||||
|
pub submapper: u8,
|
||||||
|
pub is_nes2: bool,
|
||||||
|
pub prg_rom_banks_16k: u16,
|
||||||
|
pub chr_rom_banks_8k: u16,
|
||||||
|
pub has_trainer: bool,
|
||||||
|
pub has_battery: bool,
|
||||||
|
pub mirroring: Mirroring,
|
||||||
|
pub prg_ram_shift: u8,
|
||||||
|
pub prg_nvram_shift: u8,
|
||||||
|
pub chr_ram_shift: u8,
|
||||||
|
pub chr_nvram_shift: u8,
|
||||||
|
pub cpu_ppu_timing_mode: u8,
|
||||||
|
pub vs_hardware_type: u8,
|
||||||
|
pub vs_ppu_type: u8,
|
||||||
|
pub misc_rom_count: u8,
|
||||||
|
pub default_expansion_device: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_header(bytes: &[u8]) -> Result<InesHeader, String> {
|
||||||
|
if bytes.len() < INES_HEADER_LEN {
|
||||||
|
return Err("invalid file: too small for iNES header".to_string());
|
||||||
|
}
|
||||||
|
if &bytes[0..4] != b"NES\x1A" {
|
||||||
|
return Err("invalid ROM: missing NES header".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let flags6 = bytes[6];
|
||||||
|
let flags7 = bytes[7];
|
||||||
|
let flags8 = bytes[8];
|
||||||
|
let flags9 = bytes[9];
|
||||||
|
let flags10 = bytes[10];
|
||||||
|
let flags11 = bytes[11];
|
||||||
|
let flags12 = bytes[12];
|
||||||
|
let flags13 = bytes[13];
|
||||||
|
let flags14 = bytes[14];
|
||||||
|
let flags15 = bytes[15];
|
||||||
|
|
||||||
|
let is_nes2 = (flags7 & 0b0000_1100) == 0b0000_1000;
|
||||||
|
|
||||||
|
let mapper = ((flags6 >> 4) as u16)
|
||||||
|
| ((flags7 & 0xF0) as u16)
|
||||||
|
| if is_nes2 {
|
||||||
|
((flags8 & 0x0F) as u16) << 8
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let submapper = if is_nes2 { flags8 >> 4 } else { 0 };
|
||||||
|
|
||||||
|
let prg_rom_banks_16k = if is_nes2 {
|
||||||
|
let upper = (flags9 & 0x0F) as u16;
|
||||||
|
if upper == 0x0F {
|
||||||
|
return Err("NES 2.0 exponent/multiplier PRG sizes are not supported".to_string());
|
||||||
|
}
|
||||||
|
(bytes[4] as u16) | (upper << 8)
|
||||||
|
} else {
|
||||||
|
bytes[4] as u16
|
||||||
|
};
|
||||||
|
|
||||||
|
let chr_rom_banks_8k = if is_nes2 {
|
||||||
|
let upper = ((flags9 >> 4) & 0x0F) as u16;
|
||||||
|
if upper == 0x0F {
|
||||||
|
return Err("NES 2.0 exponent/multiplier CHR sizes are not supported".to_string());
|
||||||
|
}
|
||||||
|
(bytes[5] as u16) | (upper << 8)
|
||||||
|
} else {
|
||||||
|
bytes[5] as u16
|
||||||
|
};
|
||||||
|
|
||||||
|
let mirroring = if (flags6 & 0b0000_1000) != 0 {
|
||||||
|
Mirroring::FourScreen
|
||||||
|
} else if (flags6 & 0b0000_0001) != 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(InesHeader {
|
||||||
|
mapper,
|
||||||
|
submapper,
|
||||||
|
is_nes2,
|
||||||
|
prg_rom_banks_16k,
|
||||||
|
chr_rom_banks_8k,
|
||||||
|
has_trainer: (flags6 & 0b0000_0100) != 0,
|
||||||
|
has_battery: (flags6 & 0b0000_0010) != 0,
|
||||||
|
mirroring,
|
||||||
|
prg_ram_shift: if is_nes2 { flags10 & 0x0F } else { 0 },
|
||||||
|
prg_nvram_shift: if is_nes2 { (flags10 >> 4) & 0x0F } else { 0 },
|
||||||
|
chr_ram_shift: if is_nes2 { flags11 & 0x0F } else { 0 },
|
||||||
|
chr_nvram_shift: if is_nes2 { (flags11 >> 4) & 0x0F } else { 0 },
|
||||||
|
cpu_ppu_timing_mode: if is_nes2 { flags12 & 0x03 } else { 0 },
|
||||||
|
vs_hardware_type: if is_nes2 { (flags13 >> 4) & 0x0F } else { 0 },
|
||||||
|
vs_ppu_type: if is_nes2 { flags13 & 0x0F } else { 0 },
|
||||||
|
misc_rom_count: if is_nes2 { flags14 & 0x03 } else { 0 },
|
||||||
|
default_expansion_device: if is_nes2 { flags15 & 0x3F } else { 0 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct InesRom {
|
||||||
|
pub header: InesHeader,
|
||||||
|
pub prg_rom: Vec<u8>,
|
||||||
|
pub chr_data: Vec<u8>,
|
||||||
|
pub chr_is_ram: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_rom(bytes: &[u8]) -> Result<InesRom, String> {
|
||||||
|
let header = parse_header(bytes)?;
|
||||||
|
|
||||||
|
let prg_len = header.prg_rom_banks_16k as usize * PRG_BANK_16K;
|
||||||
|
let chr_len = header.chr_rom_banks_8k as usize * CHR_BANK_8K;
|
||||||
|
|
||||||
|
let mut offset = INES_HEADER_LEN;
|
||||||
|
if header.has_trainer {
|
||||||
|
offset = offset
|
||||||
|
.checked_add(TRAINER_LEN)
|
||||||
|
.ok_or_else(|| "ROM size overflow".to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prg_end = offset
|
||||||
|
.checked_add(prg_len)
|
||||||
|
.ok_or_else(|| "ROM size overflow".to_string())?;
|
||||||
|
if bytes.len() < prg_end {
|
||||||
|
return Err("invalid ROM: truncated PRG segment".to_string());
|
||||||
|
}
|
||||||
|
let prg_rom = bytes[offset..prg_end].to_vec();
|
||||||
|
|
||||||
|
let chr_end = prg_end
|
||||||
|
.checked_add(chr_len)
|
||||||
|
.ok_or_else(|| "ROM size overflow".to_string())?;
|
||||||
|
if bytes.len() < chr_end {
|
||||||
|
return Err("invalid ROM: truncated CHR segment".to_string());
|
||||||
|
}
|
||||||
|
let chr_data = if chr_len == 0 {
|
||||||
|
vec![0; CHR_BANK_8K]
|
||||||
|
} else {
|
||||||
|
bytes[prg_end..chr_end].to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(InesRom {
|
||||||
|
header,
|
||||||
|
prg_rom,
|
||||||
|
chr_data,
|
||||||
|
chr_is_ram: chr_len == 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::{Mirroring, parse_header, parse_rom};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ines1_header_basics() {
|
||||||
|
let mut rom = vec![0u8; 16 + (2 * 16 * 1024) + (8 * 1024)];
|
||||||
|
rom[0..4].copy_from_slice(b"NES\x1A");
|
||||||
|
rom[4] = 2;
|
||||||
|
rom[5] = 1;
|
||||||
|
rom[6] = 0b0001_0001;
|
||||||
|
rom[7] = 0b0010_0000;
|
||||||
|
|
||||||
|
let header = parse_header(&rom).expect("header should parse");
|
||||||
|
assert_eq!(header.mapper, 0x21);
|
||||||
|
assert_eq!(header.prg_rom_banks_16k, 2);
|
||||||
|
assert_eq!(header.chr_rom_banks_8k, 1);
|
||||||
|
assert_eq!(header.mirroring, Mirroring::Vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_chr_ram_rom() {
|
||||||
|
let mut rom = vec![0u8; 16 + (16 * 1024)];
|
||||||
|
rom[0..4].copy_from_slice(b"NES\x1A");
|
||||||
|
rom[4] = 1;
|
||||||
|
rom[5] = 0;
|
||||||
|
|
||||||
|
let parsed = parse_rom(&rom).expect("ROM should parse");
|
||||||
|
assert_eq!(parsed.prg_rom.len(), 16 * 1024);
|
||||||
|
assert_eq!(parsed.chr_data.len(), 8 * 1024);
|
||||||
|
assert!(parsed.chr_is_ram);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nes2_extended_fields() {
|
||||||
|
let mut rom = vec![0u8; 16 + (16 * 1024)];
|
||||||
|
rom[0..4].copy_from_slice(b"NES\x1A");
|
||||||
|
rom[4] = 1;
|
||||||
|
rom[5] = 0;
|
||||||
|
rom[7] = 0x08; // NES2 marker
|
||||||
|
rom[10] = 0x54;
|
||||||
|
rom[11] = 0x32;
|
||||||
|
rom[12] = 0x01;
|
||||||
|
rom[13] = 0xBA;
|
||||||
|
rom[14] = 0x02;
|
||||||
|
rom[15] = 0x1C;
|
||||||
|
|
||||||
|
let header = parse_header(&rom).expect("NES2 header should parse");
|
||||||
|
assert!(header.is_nes2);
|
||||||
|
assert_eq!(header.prg_ram_shift, 4);
|
||||||
|
assert_eq!(header.prg_nvram_shift, 5);
|
||||||
|
assert_eq!(header.chr_ram_shift, 2);
|
||||||
|
assert_eq!(header.chr_nvram_shift, 3);
|
||||||
|
assert_eq!(header.cpu_ppu_timing_mode, 1);
|
||||||
|
assert_eq!(header.vs_hardware_type, 0xB);
|
||||||
|
assert_eq!(header.vs_ppu_type, 0xA);
|
||||||
|
assert_eq!(header.misc_rom_count, 2);
|
||||||
|
assert_eq!(header.default_expansion_device, 0x1C);
|
||||||
|
}
|
||||||
|
}
|
||||||
253
src/native_core/mapper/core.rs
Normal file
253
src/native_core/mapper/core.rs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
use crate::native_core::ines::Mirroring;
|
||||||
|
|
||||||
|
const MAPPER_STATE_SECTION_MAGIC: [u8; 4] = *b"MSS1";
|
||||||
|
const MAPPER_STATE_SECTION_VERSION: u8 = 1;
|
||||||
|
|
||||||
|
pub trait Mapper {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8;
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8);
|
||||||
|
fn cpu_read_low(&self, _addr: u16) -> Option<u8> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn cpu_write_low(&mut self, _addr: u16, _value: u8) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8;
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8);
|
||||||
|
fn mirroring(&self) -> Mirroring;
|
||||||
|
fn map_nametable_addr(&self, _addr: u16) -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn clock_cpu(&mut self, _cycles: u8) {}
|
||||||
|
fn clock_scanline(&mut self) {}
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>);
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct MapperStateSectionWriter<'a> {
|
||||||
|
out: &'a mut Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MapperStateSectionWriter<'a> {
|
||||||
|
pub(super) fn new(out: &'a mut Vec<u8>) -> Self {
|
||||||
|
Self { out }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn write_bytes(&mut self, bytes: &[u8]) {
|
||||||
|
// Unified section envelope:
|
||||||
|
// magic (4), version (1), payload_len (4), payload (N).
|
||||||
|
self.out.extend_from_slice(&MAPPER_STATE_SECTION_MAGIC);
|
||||||
|
self.out.push(MAPPER_STATE_SECTION_VERSION);
|
||||||
|
self.out
|
||||||
|
.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||||
|
self.out.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct MapperStateSectionReader<'a> {
|
||||||
|
data: &'a [u8],
|
||||||
|
cursor: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MapperStateSectionReader<'a> {
|
||||||
|
pub(super) fn new(data: &'a [u8]) -> Self {
|
||||||
|
Self { data, cursor: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_bytes(&mut self) -> Result<&'a [u8], String> {
|
||||||
|
let rem = self.data.len().saturating_sub(self.cursor);
|
||||||
|
let (len, header_len) =
|
||||||
|
if rem >= 9 && self.data[self.cursor..self.cursor + 4] == MAPPER_STATE_SECTION_MAGIC {
|
||||||
|
let version = self.data[self.cursor + 4];
|
||||||
|
if version != MAPPER_STATE_SECTION_VERSION {
|
||||||
|
return Err(format!(
|
||||||
|
"unsupported mapper state section version {}",
|
||||||
|
version
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let len = u32::from_le_bytes([
|
||||||
|
self.data[self.cursor + 5],
|
||||||
|
self.data[self.cursor + 6],
|
||||||
|
self.data[self.cursor + 7],
|
||||||
|
self.data[self.cursor + 8],
|
||||||
|
]) as usize;
|
||||||
|
(len, 9usize)
|
||||||
|
} else if rem >= 4 {
|
||||||
|
// Backward-compatible legacy section envelope:
|
||||||
|
// payload_len (4), payload (N).
|
||||||
|
let len = u32::from_le_bytes([
|
||||||
|
self.data[self.cursor],
|
||||||
|
self.data[self.cursor + 1],
|
||||||
|
self.data[self.cursor + 2],
|
||||||
|
self.data[self.cursor + 3],
|
||||||
|
]) as usize;
|
||||||
|
(len, 4usize)
|
||||||
|
} else {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
};
|
||||||
|
self.cursor += header_len;
|
||||||
|
let end = self
|
||||||
|
.cursor
|
||||||
|
.checked_add(len)
|
||||||
|
.ok_or_else(|| "mapper state cursor overflow".to_string())?;
|
||||||
|
if end > self.data.len() {
|
||||||
|
return Err("mapper state payload has invalid length".to_string());
|
||||||
|
}
|
||||||
|
let out = &self.data[self.cursor..end];
|
||||||
|
self.cursor = end;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn at_end(&self) -> bool {
|
||||||
|
self.cursor == self.data.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn safe_mod(value: usize, modulo: usize) -> usize {
|
||||||
|
if modulo == 0 { 0 } else { value % modulo }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_bank(data: &[u8], bank_size: usize, bank: usize, offset: usize) -> u8 {
|
||||||
|
if data.is_empty() || bank_size == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let total_banks = data.len() / bank_size;
|
||||||
|
let bank_idx = safe_mod(bank, total_banks.max(1));
|
||||||
|
let idx = bank_idx * bank_size + safe_mod(offset, bank_size);
|
||||||
|
data.get(idx).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn write_state_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||||
|
MapperStateSectionWriter::new(out).write_bytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn read_state_bytes<'a>(data: &'a [u8], cursor: &mut usize) -> Result<&'a [u8], String> {
|
||||||
|
let mut rd = MapperStateSectionReader {
|
||||||
|
data,
|
||||||
|
cursor: *cursor,
|
||||||
|
};
|
||||||
|
let payload = rd.read_bytes()?;
|
||||||
|
*cursor = rd.cursor;
|
||||||
|
Ok(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn write_chr_state(out: &mut Vec<u8>, chr_data: &[u8]) {
|
||||||
|
write_state_bytes(out, chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn load_chr_state(chr_data: &mut [u8], data: &[u8]) -> Result<(), String> {
|
||||||
|
let mut rd = MapperStateSectionReader::new(data);
|
||||||
|
let payload = rd.read_bytes()?;
|
||||||
|
if payload.len() != chr_data.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
chr_data.copy_from_slice(payload);
|
||||||
|
if !rd.at_end() {
|
||||||
|
return Err("mapper state has trailing bytes".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn encode_mirroring(mirroring: Mirroring) -> u8 {
|
||||||
|
match mirroring {
|
||||||
|
Mirroring::Horizontal => 0,
|
||||||
|
Mirroring::Vertical => 1,
|
||||||
|
Mirroring::FourScreen => 2,
|
||||||
|
Mirroring::OneScreenLow => 3,
|
||||||
|
Mirroring::OneScreenHigh => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn decode_mirroring(value: u8) -> Mirroring {
|
||||||
|
match value {
|
||||||
|
1 => Mirroring::Vertical,
|
||||||
|
2 => Mirroring::FourScreen,
|
||||||
|
3 => Mirroring::OneScreenLow,
|
||||||
|
4 => Mirroring::OneScreenHigh,
|
||||||
|
_ => Mirroring::Horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct VrcIrqRegisters<'a> {
|
||||||
|
pub latch: &'a mut u8,
|
||||||
|
pub counter: &'a mut u8,
|
||||||
|
pub enabled: &'a mut bool,
|
||||||
|
pub enabled_after_ack: &'a mut bool,
|
||||||
|
pub mode_cpu: &'a mut bool,
|
||||||
|
pub pending: &'a mut bool,
|
||||||
|
pub prescaler: &'a mut i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn vrc_irq_tick(counter: &mut u8, latch: u8, pending: &mut bool) {
|
||||||
|
if *counter == 0xFF {
|
||||||
|
*counter = latch;
|
||||||
|
*pending = true;
|
||||||
|
} else {
|
||||||
|
*counter = counter.wrapping_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn vrc_irq_write_control(value: u8, irq: VrcIrqRegisters<'_>) {
|
||||||
|
let VrcIrqRegisters {
|
||||||
|
latch,
|
||||||
|
counter,
|
||||||
|
enabled,
|
||||||
|
enabled_after_ack,
|
||||||
|
mode_cpu,
|
||||||
|
pending,
|
||||||
|
prescaler,
|
||||||
|
} = irq;
|
||||||
|
*mode_cpu = (value & 0x04) != 0;
|
||||||
|
*enabled = (value & 0x02) != 0;
|
||||||
|
*enabled_after_ack = (value & 0x01) != 0;
|
||||||
|
*pending = false;
|
||||||
|
*prescaler = 341;
|
||||||
|
if *enabled {
|
||||||
|
*counter = *latch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn vrc_irq_ack(irq: VrcIrqRegisters<'_>) {
|
||||||
|
let VrcIrqRegisters {
|
||||||
|
enabled,
|
||||||
|
enabled_after_ack,
|
||||||
|
pending,
|
||||||
|
prescaler,
|
||||||
|
..
|
||||||
|
} = irq;
|
||||||
|
*pending = false;
|
||||||
|
*enabled = *enabled_after_ack;
|
||||||
|
*prescaler = 341;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn vrc_irq_clock(cycles: u8, irq: VrcIrqRegisters<'_>) {
|
||||||
|
let VrcIrqRegisters {
|
||||||
|
latch,
|
||||||
|
counter,
|
||||||
|
enabled,
|
||||||
|
mode_cpu,
|
||||||
|
pending,
|
||||||
|
prescaler,
|
||||||
|
..
|
||||||
|
} = irq;
|
||||||
|
if !*enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for _ in 0..cycles {
|
||||||
|
if *mode_cpu {
|
||||||
|
vrc_irq_tick(counter, *latch, pending);
|
||||||
|
} else {
|
||||||
|
*prescaler -= 3;
|
||||||
|
if *prescaler <= 0 {
|
||||||
|
*prescaler += 341;
|
||||||
|
vrc_irq_tick(counter, *latch, pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/native_core/mapper/mappers/axrom.rs
Normal file
91
src/native_core/mapper/mappers/axrom.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Axrom {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
bank_select: u8,
|
||||||
|
one_screen_hi: bool,
|
||||||
|
bus_conflicts_and: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Axrom {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let bus_conflicts_and = rom.header.mapper == 7 && rom.header.submapper == 2;
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
bank_select: 0,
|
||||||
|
one_screen_hi: false,
|
||||||
|
bus_conflicts_and,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Axrom {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
self.bank_select as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
let latched = if self.bus_conflicts_and {
|
||||||
|
value & self.cpu_read(addr)
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
};
|
||||||
|
self.bank_select = latched & 0x07;
|
||||||
|
self.one_screen_hi = (latched & 0x10) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
if self.one_screen_hi {
|
||||||
|
Mirroring::OneScreenHigh
|
||||||
|
} else {
|
||||||
|
Mirroring::OneScreenLow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.bank_select);
|
||||||
|
out.push(u8::from(self.one_screen_hi));
|
||||||
|
out.push(u8::from(self.bus_conflicts_and));
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_select = data[0];
|
||||||
|
self.one_screen_hi = data[1] != 0;
|
||||||
|
self.bus_conflicts_and = data[2] != 0;
|
||||||
|
load_chr_state(&mut self.chr_data, &data[3..])
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/native_core/mapper/mappers/bandai70_152.rs
Normal file
119
src/native_core/mapper/mappers/bandai70_152.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Bandai70_152 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank: u8,
|
||||||
|
one_screen_hi: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bandai70_152 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank: 0,
|
||||||
|
one_screen_hi: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_16k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Bandai70_152 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank_count_16k().saturating_sub(1),
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.prg_bank = value & 0x0F;
|
||||||
|
self.chr_bank = (value >> 4) & 0x0F;
|
||||||
|
self.one_screen_hi = Some((value & 0x80) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let bank_idx = safe_mod(self.chr_bank as usize, total_banks);
|
||||||
|
let idx = bank_idx * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match self.one_screen_hi {
|
||||||
|
Some(true) => Mirroring::OneScreenHigh,
|
||||||
|
Some(false) => Mirroring::OneScreenLow,
|
||||||
|
None => self.mirroring_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
out.push(match self.one_screen_hi {
|
||||||
|
Some(true) => 2,
|
||||||
|
Some(false) => 1,
|
||||||
|
None => 0,
|
||||||
|
});
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank = data[1];
|
||||||
|
self.one_screen_hi = match data[2] {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(false),
|
||||||
|
2 => Some(true),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
load_chr_state(&mut self.chr_data, &data[3..])
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/native_core/mapper/mappers/bnrom34.rs
Normal file
112
src/native_core/mapper/mappers/bnrom34.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Bnrom34 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank_0_4k: u8,
|
||||||
|
chr_bank_1_4k: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bnrom34 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank_0_4k: 0,
|
||||||
|
chr_bank_1_4k: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Bnrom34 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.prg_bank = value & 0x0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
match addr {
|
||||||
|
0x7FFD => {
|
||||||
|
self.prg_bank = value & 0x0F;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
0x7FFE => {
|
||||||
|
self.chr_bank_0_4k = value & 0x0F;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
0x7FFF => {
|
||||||
|
self.chr_bank_1_4k = value & 0x0F;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank_0_4k as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_1_4k as usize
|
||||||
|
};
|
||||||
|
read_bank(&self.chr_data, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank_0_4k as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_1_4k as usize
|
||||||
|
};
|
||||||
|
let total = (self.chr_data.len() / 0x1000).max(1);
|
||||||
|
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank_0_4k);
|
||||||
|
out.push(self.chr_bank_1_4k);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank_0_4k = data[1];
|
||||||
|
self.chr_bank_1_4k = data[2];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[3..])
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/native_core/mapper/mappers/camerica71.rs
Normal file
114
src/native_core/mapper/mappers/camerica71.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Camerica71 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
submapper: u8,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
one_screen_hi: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camerica71 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let submapper = rom.header.submapper;
|
||||||
|
let one_screen_hi = if submapper == 1 { Some(false) } else { None };
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
submapper,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
one_screen_hi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_16k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Camerica71 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank_count_16k() - 1,
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if self.submapper == 1 && (0x9000..=0x9FFF).contains(&addr) {
|
||||||
|
self.one_screen_hi = Some((value & 0x10) != 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.prg_bank = value & 0x0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match self.one_screen_hi {
|
||||||
|
Some(true) => Mirroring::OneScreenHigh,
|
||||||
|
Some(false) => Mirroring::OneScreenLow,
|
||||||
|
None => self.mirroring_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.submapper);
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(match self.one_screen_hi {
|
||||||
|
Some(true) => 2,
|
||||||
|
Some(false) => 1,
|
||||||
|
None => 0,
|
||||||
|
});
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.submapper = data[0];
|
||||||
|
self.prg_bank = data[1];
|
||||||
|
self.one_screen_hi = match data[2] {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(false),
|
||||||
|
2 => Some(true),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
load_chr_state(&mut self.chr_data, &data[3..])
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/native_core/mapper/mappers/cnrom.rs
Normal file
100
src/native_core/mapper/mappers/cnrom.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Cnrom {
|
||||||
|
submapper: u8,
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
chr_bank: u8,
|
||||||
|
bus_conflicts_and: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cnrom {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let bus_conflicts_and = rom.header.mapper == 3 && rom.header.submapper == 2;
|
||||||
|
Self {
|
||||||
|
submapper: rom.header.submapper,
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
chr_bank: 0,
|
||||||
|
bus_conflicts_and,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_banks(&self) -> usize {
|
||||||
|
(self.chr_data.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Cnrom {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
((addr - 0x8000) as usize) / 0x4000,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
let value = if self.bus_conflicts_and {
|
||||||
|
value & self.cpu_read(addr)
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
};
|
||||||
|
let max = self.chr_banks() as u8;
|
||||||
|
self.chr_bank = if max == 0 { 0 } else { value % max };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank = safe_mod(self.chr_bank as usize, self.chr_banks());
|
||||||
|
let idx = bank * 0x2000 + (addr as usize & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
out.push(self.submapper);
|
||||||
|
out.push(u8::from(self.bus_conflicts_and));
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.chr_bank = data[0];
|
||||||
|
self.submapper = data[1];
|
||||||
|
self.bus_conflicts_and = data[2] != 0;
|
||||||
|
load_chr_state(&mut self.chr_data, &data[3..])
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/native_core/mapper/mappers/color_dreams11.rs
Normal file
88
src/native_core/mapper/mappers/color_dreams11.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct ColorDreams11 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorDreams11 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for ColorDreams11 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
let latched = value & self.cpu_read(addr);
|
||||||
|
self.prg_bank = latched & 0x03;
|
||||||
|
self.chr_bank = (latched >> 4) & 0x0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let bank_idx = safe_mod(self.chr_bank as usize, total_banks);
|
||||||
|
let idx = bank_idx * 0x2000 + (addr as usize & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank = data[1];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[2..])
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/native_core/mapper/mappers/cprom13.rs
Normal file
88
src/native_core/mapper/mappers/cprom13.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Cprom13 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_ram: Vec<u8>,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
chr_bank_hi_4k: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cprom13 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let mut chr_ram = rom.chr_data;
|
||||||
|
if chr_ram.len() < 0x4000 {
|
||||||
|
chr_ram.resize(0x4000, 0);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
chr_bank_hi_4k: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Cprom13 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
((addr - 0x8000) as usize) / 0x4000,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.chr_bank_hi_4k = value & 0x03;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
0usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_hi_4k as usize
|
||||||
|
};
|
||||||
|
read_bank(&self.chr_ram, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
0usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_hi_4k as usize
|
||||||
|
};
|
||||||
|
let total = (self.chr_ram.len() / 0x1000).max(1);
|
||||||
|
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||||
|
if let Some(cell) = self.chr_ram.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.chr_bank_hi_4k);
|
||||||
|
write_chr_state(out, &self.chr_ram);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.chr_bank_hi_4k = data[0];
|
||||||
|
load_chr_state(&mut self.chr_ram, &data[1..])
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/native_core/mapper/mappers/crazy_climber180.rs
Normal file
78
src/native_core/mapper/mappers/crazy_climber180.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct CrazyClimber180 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
bank_select: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CrazyClimber180 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
bank_select: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for CrazyClimber180 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(&self.prg_rom, 0x4000, 0, (addr as usize) - 0x8000)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.bank_select as usize,
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.bank_select = value & 0x07;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.bank_select);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_select = data[0];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[1..])
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/native_core/mapper/mappers/fme7.rs
Normal file
19
src/native_core/mapper/mappers/fme7.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Fme7 {
|
||||||
|
pub(super) prg_rom: Vec<u8>,
|
||||||
|
pub(super) chr_data: Vec<u8>,
|
||||||
|
pub(super) chr_is_ram: bool,
|
||||||
|
pub(super) mirroring: Mirroring,
|
||||||
|
pub(super) command: u8,
|
||||||
|
pub(super) chr_banks: [u8; 8],
|
||||||
|
pub(super) prg_banks: [u8; 3],
|
||||||
|
pub(super) low_bank: u8,
|
||||||
|
pub(super) low_is_ram: bool,
|
||||||
|
pub(super) low_ram_enabled: bool,
|
||||||
|
pub(super) low_ram: Vec<u8>,
|
||||||
|
pub(super) irq_counter: u16,
|
||||||
|
pub(super) irq_enabled: bool,
|
||||||
|
pub(super) irq_counter_enabled: bool,
|
||||||
|
pub(super) irq_pending: bool,
|
||||||
|
}
|
||||||
85
src/native_core/mapper/mappers/gxrom.rs
Normal file
85
src/native_core/mapper/mappers/gxrom.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Gxrom {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank: u8,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gxrom {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank: 0,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Gxrom {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.prg_bank = (value >> 4) & 0x03;
|
||||||
|
self.chr_bank = value & 0x03;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let idx = safe_mod(self.chr_bank as usize, total) * 0x2000 + (addr as usize & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() != 2 {
|
||||||
|
return Err("mapper state payload has invalid length".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank = data[1];
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
295
src/native_core/mapper/mappers/mapper105.rs
Normal file
295
src/native_core/mapper/mappers/mapper105.rs
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper105 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
shift_reg: u8,
|
||||||
|
shift_count: u8,
|
||||||
|
control: u8,
|
||||||
|
reg_a: u8,
|
||||||
|
reg_b: u8,
|
||||||
|
wram_disabled: bool,
|
||||||
|
prg_unlocked: bool,
|
||||||
|
saw_i_low: bool,
|
||||||
|
irq_counter: u32, // 30-bit counter
|
||||||
|
irq_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper105 {
|
||||||
|
pub(crate) const IRQ_THRESHOLD: u32 = 0x2800_0000; // Official Nintendo World Championships DIP setting.
|
||||||
|
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: if rom.chr_data.is_empty() {
|
||||||
|
vec![0; 0x2000]
|
||||||
|
} else {
|
||||||
|
rom.chr_data
|
||||||
|
},
|
||||||
|
prg_ram: vec![0; 0x2000],
|
||||||
|
shift_reg: 0,
|
||||||
|
shift_count: 0,
|
||||||
|
control: 0x0C,
|
||||||
|
reg_a: 0x10, // I bit high after reset keeps timer halted.
|
||||||
|
reg_b: 0,
|
||||||
|
wram_disabled: false,
|
||||||
|
prg_unlocked: false,
|
||||||
|
saw_i_low: false,
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_mode(&self) -> u8 {
|
||||||
|
(self.control >> 2) & 0x03
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timer_halted(&self) -> bool {
|
||||||
|
(self.reg_a & 0x10) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outer_chip_selected(&self) -> bool {
|
||||||
|
(self.reg_a & 0x08) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outer_32k_bank(&self) -> usize {
|
||||||
|
((self.reg_a >> 1) & 0x03) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_value(&self) -> usize {
|
||||||
|
(self.reg_b & 0x0F) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_16k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_chip_banks_16k(&self) -> usize {
|
||||||
|
(self.prg_bank_count_16k() / 2).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn second_chip_base_16k(&self) -> usize {
|
||||||
|
self.first_chip_banks_16k()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn second_chip_banks_16k(&self) -> usize {
|
||||||
|
self.prg_bank_count_16k()
|
||||||
|
.saturating_sub(self.second_chip_base_16k())
|
||||||
|
.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_reg_a_write(&mut self, value: u8) {
|
||||||
|
let previous_i = (self.reg_a & 0x10) != 0;
|
||||||
|
let next_i = (value & 0x10) != 0;
|
||||||
|
self.reg_a = value & 0x1F;
|
||||||
|
|
||||||
|
if !next_i {
|
||||||
|
self.saw_i_low = true;
|
||||||
|
}
|
||||||
|
if previous_i && !next_i {
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
if !previous_i && next_i && self.saw_i_low {
|
||||||
|
self.prg_unlocked = true;
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
if next_i {
|
||||||
|
self.irq_counter = 0;
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_serial(&mut self, addr: u16, value: u8) {
|
||||||
|
if (value & 0x80) != 0 {
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
self.control |= 0x0C;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shift_reg |= (value & 1) << self.shift_count;
|
||||||
|
self.shift_count = self.shift_count.wrapping_add(1);
|
||||||
|
if self.shift_count < 5 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let latched = self.shift_reg & 0x1F;
|
||||||
|
match (addr >> 13) & 0x03 {
|
||||||
|
0 => self.control = latched,
|
||||||
|
1 => self.on_reg_a_write(latched),
|
||||||
|
2 => {}
|
||||||
|
_ => {
|
||||||
|
self.reg_b = latched;
|
||||||
|
self.wram_disabled = (latched & 0x10) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper105 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.prg_unlocked {
|
||||||
|
// Reset/power-on maps a fixed 32KiB window from the first PRG chip.
|
||||||
|
return read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
0,
|
||||||
|
(addr as usize).saturating_sub(0x8000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.outer_chip_selected() {
|
||||||
|
let bank32 = self.outer_32k_bank();
|
||||||
|
return read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
bank32,
|
||||||
|
(addr as usize).saturating_sub(0x8000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let second_base = self.second_chip_base_16k();
|
||||||
|
let second_count = self.second_chip_banks_16k();
|
||||||
|
let prg_bank = self.prg_bank_value();
|
||||||
|
|
||||||
|
let bank16 = match self.prg_mode() {
|
||||||
|
0 | 1 => {
|
||||||
|
let bank32 = prg_bank >> 1;
|
||||||
|
if addr < 0xC000 {
|
||||||
|
second_base + safe_mod(bank32 * 2, second_count)
|
||||||
|
} else {
|
||||||
|
second_base + safe_mod(bank32 * 2 + 1, second_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
if addr < 0xC000 {
|
||||||
|
second_base
|
||||||
|
} else {
|
||||||
|
second_base + safe_mod(prg_bank, second_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if addr < 0xC000 {
|
||||||
|
second_base + safe_mod(prg_bank, second_count)
|
||||||
|
} else {
|
||||||
|
second_base + second_count.saturating_sub(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
read_bank(&self.prg_rom, 0x4000, bank16, (addr as usize) & 0x3FFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.write_serial(addr, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) && !self.wram_disabled {
|
||||||
|
let idx = (addr as usize) & 0x1FFF;
|
||||||
|
return self.prg_ram.get(idx).copied();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) && !self.wram_disabled {
|
||||||
|
let idx = (addr as usize) & 0x1FFF;
|
||||||
|
if let Some(cell) = self.prg_ram.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match self.control & 0x03 {
|
||||||
|
0 => Mirroring::OneScreenLow,
|
||||||
|
1 => Mirroring::OneScreenHigh,
|
||||||
|
2 => Mirroring::Vertical,
|
||||||
|
_ => Mirroring::Horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
if self.timer_halted() || self.irq_pending {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let previous = self.irq_counter;
|
||||||
|
self.irq_counter = (self.irq_counter.wrapping_add(cycles as u32)) & 0x3FFF_FFFF;
|
||||||
|
if previous < Self::IRQ_THRESHOLD && self.irq_counter >= Self::IRQ_THRESHOLD {
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.shift_reg);
|
||||||
|
out.push(self.shift_count);
|
||||||
|
out.push(self.control);
|
||||||
|
out.push(self.reg_a);
|
||||||
|
out.push(self.reg_b);
|
||||||
|
out.push(u8::from(self.wram_disabled));
|
||||||
|
out.push(u8::from(self.prg_unlocked));
|
||||||
|
out.push(u8::from(self.saw_i_low));
|
||||||
|
out.extend_from_slice(&self.irq_counter.to_le_bytes());
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 13 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.shift_reg = data[0];
|
||||||
|
self.shift_count = data[1];
|
||||||
|
self.control = data[2];
|
||||||
|
self.reg_a = data[3];
|
||||||
|
self.reg_b = data[4];
|
||||||
|
self.wram_disabled = data[5] != 0;
|
||||||
|
self.prg_unlocked = data[6] != 0;
|
||||||
|
self.saw_i_low = data[7] != 0;
|
||||||
|
self.irq_counter = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) & 0x3FFF_FFFF;
|
||||||
|
self.irq_pending = data[12] != 0;
|
||||||
|
|
||||||
|
let mut cursor = 13usize;
|
||||||
|
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if prg_ram.len() != self.prg_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.prg_ram.copy_from_slice(prg_ram);
|
||||||
|
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/native_core/mapper/mappers/mapper118.rs
Normal file
77
src/native_core/mapper/mappers/mapper118.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper118 {
|
||||||
|
mmc3: Mmc3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper118 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
mmc3: Mmc3::new(rom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper118 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.mmc3.cpu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.mmc3.cpu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
self.mmc3.cpu_read_low(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
self.mmc3.cpu_write_low(addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.mmc3.ppu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.mmc3.ppu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mmc3.mirroring()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_nametable_addr(&self, addr: u16) -> Option<usize> {
|
||||||
|
if !(0x2000..=0x3EFF).contains(&addr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// TxSROM-class boards route CHR bank bit 7 (A17) to CIRAM A10 for NT fetches.
|
||||||
|
// The board responds to $2000-$2FFF the same as MMC3's $0000-$0FFF CHR decode.
|
||||||
|
let rel = (addr - 0x2000) & 0x0FFF;
|
||||||
|
let page = (rel / 0x0400) as usize; // NT0..NT3 -> 1KB pages 0..3
|
||||||
|
let offset = (rel & 0x03FF) as usize;
|
||||||
|
let bank = self.mmc3.chr_bank_for_1k_page(page) as u8;
|
||||||
|
let ciram_page = ((bank >> 7) & 1) as usize;
|
||||||
|
Some(ciram_page * 0x0400 + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
self.mmc3.clock_scanline();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
self.mmc3.needs_ppu_a12_clock()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
self.mmc3.poll_irq()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
self.mmc3.save_state(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
self.mmc3.load_state(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/native_core/mapper/mappers/mapper140.rs
Normal file
90
src/native_core/mapper/mappers/mapper140.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper140 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper140 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper140 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.chr_bank = value & 0x0F;
|
||||||
|
self.prg_bank = (value >> 4) & 0x03;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let idx = safe_mod(self.chr_bank as usize, total) * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank = data[1];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[2..])
|
||||||
|
}
|
||||||
|
}
|
||||||
253
src/native_core/mapper/mappers/mapper155.rs
Normal file
253
src/native_core/mapper/mappers/mapper155.rs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper155 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
shift_reg: u8,
|
||||||
|
shift_count: u8,
|
||||||
|
control: u8,
|
||||||
|
chr_bank0: u8,
|
||||||
|
chr_bank1: u8,
|
||||||
|
prg_bank: u8,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper155 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
prg_ram: vec![0; 0x2000],
|
||||||
|
shift_reg: 0,
|
||||||
|
shift_count: 0,
|
||||||
|
control: 0x0C,
|
||||||
|
chr_bank0: 0,
|
||||||
|
chr_bank1: 0,
|
||||||
|
prg_bank: 0,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_mode(&self) -> u8 {
|
||||||
|
(self.control >> 2) & 0x03
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_mode(&self) -> u8 {
|
||||||
|
(self.control >> 4) & 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_16k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outer_a17_override_enabled(&self) -> bool {
|
||||||
|
(self.prg_bank & 0x10) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outer_bank_half(&self) -> usize {
|
||||||
|
((self.prg_bank >> 3) & 1) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_bank_16k_with_outer(&self, inner_bank: usize) -> usize {
|
||||||
|
let total = self.prg_bank_count_16k();
|
||||||
|
if !self.outer_a17_override_enabled() || total <= 8 {
|
||||||
|
return safe_mod(inner_bank, total);
|
||||||
|
}
|
||||||
|
let half = (total / 2).max(1);
|
||||||
|
let base = safe_mod(self.outer_bank_half(), 2) * half;
|
||||||
|
base + safe_mod(inner_bank, half)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_serial(&mut self, addr: u16, value: u8) {
|
||||||
|
if (value & 0x80) != 0 {
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
self.control |= 0x0C;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shift_reg |= (value & 1) << self.shift_count;
|
||||||
|
self.shift_count = self.shift_count.wrapping_add(1);
|
||||||
|
if self.shift_count < 5 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reg = (addr >> 13) & 0x03;
|
||||||
|
match reg {
|
||||||
|
0 => self.control = self.shift_reg & 0x1F,
|
||||||
|
1 => self.chr_bank0 = self.shift_reg & 0x1F,
|
||||||
|
2 => self.chr_bank1 = self.shift_reg & 0x1F,
|
||||||
|
_ => self.prg_bank = self.shift_reg & 0x1F, // MMC1A uses bit 4 for A17 behavior
|
||||||
|
}
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper155 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prg_mode = self.prg_mode();
|
||||||
|
if prg_mode <= 1 {
|
||||||
|
let bank32 = (self.prg_bank as usize) >> 1;
|
||||||
|
let bank16 = bank32 * 2 + usize::from(addr >= 0xC000);
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.map_bank_16k_with_outer(bank16),
|
||||||
|
(addr as usize) & 0x3FFF,
|
||||||
|
)
|
||||||
|
} else if prg_mode == 2 {
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.map_bank_16k_with_outer(0),
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.map_bank_16k_with_outer((self.prg_bank & 0x0F) as usize),
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.map_bank_16k_with_outer((self.prg_bank & 0x0F) as usize),
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let fixed_last = if self.outer_a17_override_enabled() && self.prg_bank_count_16k() > 8 {
|
||||||
|
self.prg_bank_count_16k() / 2 - 1
|
||||||
|
} else {
|
||||||
|
self.prg_bank_count_16k().saturating_sub(1)
|
||||||
|
};
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.map_bank_16k_with_outer(fixed_last),
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.write_serial(addr, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
return self.prg_ram.get((addr as usize) & 0x1FFF).copied();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
if let Some(cell) = self.prg_ram.get_mut((addr as usize) & 0x1FFF) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if self.chr_mode() == 0 {
|
||||||
|
let bank = (self.chr_bank0 as usize) >> 1;
|
||||||
|
read_bank(&self.chr_data, 0x2000, bank, addr as usize)
|
||||||
|
} else if addr < 0x1000 {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank0 as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank1 as usize,
|
||||||
|
(addr as usize) - 0x1000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let idx = if self.chr_mode() == 0 {
|
||||||
|
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let bank = safe_mod((self.chr_bank0 as usize) >> 1, total_banks);
|
||||||
|
bank * 0x2000 + ((addr as usize) & 0x1FFF)
|
||||||
|
} else {
|
||||||
|
let total_banks = (self.chr_data.len() / 0x1000).max(1);
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank0 as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank1 as usize
|
||||||
|
};
|
||||||
|
safe_mod(bank, total_banks) * 0x1000 + ((addr as usize) & 0x0FFF)
|
||||||
|
};
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match self.control & 0x03 {
|
||||||
|
0 => Mirroring::OneScreenLow,
|
||||||
|
1 => Mirroring::OneScreenHigh,
|
||||||
|
2 => Mirroring::Vertical,
|
||||||
|
3 => Mirroring::Horizontal,
|
||||||
|
_ => self.mirroring_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.shift_reg);
|
||||||
|
out.push(self.shift_count);
|
||||||
|
out.push(self.control);
|
||||||
|
out.push(self.chr_bank0);
|
||||||
|
out.push(self.chr_bank1);
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 6 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.shift_reg = data[0];
|
||||||
|
self.shift_count = data[1];
|
||||||
|
self.control = data[2];
|
||||||
|
self.chr_bank0 = data[3];
|
||||||
|
self.chr_bank1 = data[4];
|
||||||
|
self.prg_bank = data[5];
|
||||||
|
|
||||||
|
let mut cursor = 6usize;
|
||||||
|
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if prg_ram.len() != self.prg_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.prg_ram.copy_from_slice(prg_ram);
|
||||||
|
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/native_core/mapper/mappers/mapper158.rs
Normal file
75
src/native_core/mapper/mappers/mapper158.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper158 {
|
||||||
|
base: InesMapper64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper158 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
base: InesMapper64::new(rom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper158 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.base.cpu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
// Mapper 158 uses CHR-bank controlled nametable routing instead of $A000 mirroring writes.
|
||||||
|
if (0xA000..=0xBFFF).contains(&addr) && (addr & 1) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.base.cpu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.base.ppu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.base.ppu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_nametable_addr(&self, addr: u16) -> Option<usize> {
|
||||||
|
if !(0x2000..=0x3EFF).contains(&addr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rel = (addr - 0x2000) & 0x0FFF;
|
||||||
|
let page = (rel / 0x0400) as usize; // NT0..NT3
|
||||||
|
let offset = (rel & 0x03FF) as usize;
|
||||||
|
let chr_bank = self.base.chr_bank_for_page(page) as u8;
|
||||||
|
let ciram_page = ((chr_bank >> 7) & 1) as usize;
|
||||||
|
Some(ciram_page * 0x0400 + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
self.base.clock_cpu(cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
self.base.clock_scanline();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
self.base.needs_ppu_a12_clock()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
self.base.poll_irq()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
self.base.save_state(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
self.base.load_state(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/native_core/mapper/mappers/mapper184.rs
Normal file
95
src/native_core/mapper/mappers/mapper184.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper184 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
chr_bank_lo_4k: u8,
|
||||||
|
chr_bank_hi_4k: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper184 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
chr_bank_lo_4k: 0,
|
||||||
|
chr_bank_hi_4k: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper184 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
((addr - 0x8000) as usize) / 0x4000,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.chr_bank_lo_4k = value & 0x07;
|
||||||
|
self.chr_bank_hi_4k = 0x04 | ((value >> 4) & 0x07);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank_lo_4k as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_hi_4k as usize
|
||||||
|
};
|
||||||
|
read_bank(&self.chr_data, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank_lo_4k as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_hi_4k as usize
|
||||||
|
};
|
||||||
|
let total = (self.chr_data.len() / 0x1000).max(1);
|
||||||
|
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.chr_bank_lo_4k);
|
||||||
|
out.push(self.chr_bank_hi_4k);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.chr_bank_lo_4k = data[0];
|
||||||
|
self.chr_bank_hi_4k = data[1];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[2..])
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/native_core/mapper/mappers/mapper185.rs
Normal file
92
src/native_core/mapper/mappers/mapper185.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper185 {
|
||||||
|
submapper: u8,
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
latch: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper185 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
submapper: rom.header.submapper,
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
latch: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_enabled(&self) -> bool {
|
||||||
|
match self.submapper {
|
||||||
|
4..=7 => (self.latch & 0x03) == (self.submapper - 4),
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper185 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
((addr - 0x8000) as usize) / 0x4000,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
// Mapper 185 always has AND bus conflicts.
|
||||||
|
self.latch = value & self.cpu_read(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if !self.chr_enabled() {
|
||||||
|
return 0xFF;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !self.chr_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.submapper);
|
||||||
|
out.push(self.latch);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.submapper = data[0];
|
||||||
|
self.latch = data[1];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[2..])
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/native_core/mapper/mappers/mapper206.rs
Normal file
127
src/native_core/mapper/mappers/mapper206.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper206 {
|
||||||
|
pub(super) prg_rom: Vec<u8>,
|
||||||
|
pub(super) chr_data: Vec<u8>,
|
||||||
|
pub(super) chr_is_ram: bool,
|
||||||
|
pub(super) mirroring: Mirroring,
|
||||||
|
pub(super) bank_regs: [u8; 8],
|
||||||
|
pub(super) bank_select: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper206 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
bank_regs: [0; 8],
|
||||||
|
bank_select: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||||
|
let last = self.prg_bank_count_8k() - 1;
|
||||||
|
let second_last = last.saturating_sub(1);
|
||||||
|
match slot {
|
||||||
|
0 => self.bank_regs[6] as usize,
|
||||||
|
1 => self.bank_regs[7] as usize,
|
||||||
|
2 => second_last,
|
||||||
|
3 => last,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn chr_bank_for_1k_page(&self, page: usize) -> u8 {
|
||||||
|
let regs = &self.bank_regs;
|
||||||
|
match page {
|
||||||
|
0 => regs[0] & !1,
|
||||||
|
1 => (regs[0] & !1).wrapping_add(1),
|
||||||
|
2 => regs[1] & !1,
|
||||||
|
3 => (regs[1] & !1).wrapping_add(1),
|
||||||
|
4 => regs[2],
|
||||||
|
5 => regs[3],
|
||||||
|
6 => regs[4],
|
||||||
|
_ => regs[5],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper206 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let bank = self.prg_bank_for_slot(slot);
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value & 0x07,
|
||||||
|
0x8000..=0x9FFF => {
|
||||||
|
let reg = (self.bank_select & 0x07) as usize;
|
||||||
|
self.bank_regs[reg] = value;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_1k_page(page);
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x0400,
|
||||||
|
bank as usize,
|
||||||
|
(addr as usize) & 0x03FF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_1k_page(page) as usize;
|
||||||
|
let total_banks = (self.chr_data.len() / 0x0400).max(1);
|
||||||
|
let bank_idx = safe_mod(bank, total_banks);
|
||||||
|
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.bank_regs);
|
||||||
|
out.push(self.bank_select);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 9 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_regs.copy_from_slice(&data[0..8]);
|
||||||
|
self.bank_select = data[8];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[9..])
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/native_core/mapper/mappers/mapper206_submapper1.rs
Normal file
63
src/native_core/mapper/mappers/mapper206_submapper1.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper206Submapper1 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper206Submapper1 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper206Submapper1 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
0,
|
||||||
|
((addr as usize) - 0x8000) & 0x7FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
load_chr_state(&mut self.chr_data, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
313
src/native_core/mapper/mappers/mapper253.rs
Normal file
313
src/native_core/mapper/mappers/mapper253.rs
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper253 {
|
||||||
|
base: Vrc2_23,
|
||||||
|
chr_ram_2k: [u8; 0x800], // PPU pages 4/5 ($1000-$17FF): on-cart CHR-RAM overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper253 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let mut base = Vrc2_23::new_with_submapper(rom, 2); // VRC4e-style decode
|
||||||
|
base.mapper_id = 23;
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
chr_ram_2k: [0; 0x800],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper253 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.base.cpu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.base.cpu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
self.base.cpu_read_low(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
self.base.cpu_write_low(addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (0x1000..0x1800).contains(&addr) {
|
||||||
|
return self.chr_ram_2k[(addr as usize) - 0x1000];
|
||||||
|
}
|
||||||
|
self.base.ppu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (0x1000..0x1800).contains(&addr) {
|
||||||
|
self.chr_ram_2k[(addr as usize) - 0x1000] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.base.ppu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.base.mirroring()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
self.base.clock_cpu(cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
self.base.poll_irq()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
let mut base_state = Vec::new();
|
||||||
|
self.base.save_state(&mut base_state);
|
||||||
|
write_state_bytes(out, &base_state);
|
||||||
|
out.extend_from_slice(&self.chr_ram_2k);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
let base_state = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if data.len().saturating_sub(cursor) != self.chr_ram_2k.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.base.load_state(base_state)?;
|
||||||
|
self.chr_ram_2k.copy_from_slice(&data[cursor..]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fme7 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
command: 0,
|
||||||
|
chr_banks: [0; 8],
|
||||||
|
prg_banks: [0, 1, 0xFE],
|
||||||
|
low_bank: 0,
|
||||||
|
low_is_ram: false,
|
||||||
|
low_ram_enabled: false,
|
||||||
|
low_ram: vec![0; 0x8000],
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_counter_enabled: false,
|
||||||
|
irq_pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn low_ram_index(&self, addr: u16) -> usize {
|
||||||
|
let bank = (self.low_bank & 0x03) as usize;
|
||||||
|
bank * 0x2000 + ((addr as usize) & 0x1FFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Fme7 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let bank = match ((addr - 0x8000) / 0x2000) as usize {
|
||||||
|
0 => self.prg_banks[0] as usize,
|
||||||
|
1 => self.prg_banks[1] as usize,
|
||||||
|
2 => self.prg_banks[2] as usize,
|
||||||
|
_ => self.prg_bank_count_8k().saturating_sub(1),
|
||||||
|
};
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if (0x8000..=0x9FFF).contains(&addr) {
|
||||||
|
self.command = value & 0x0F;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !(0xA000..=0xBFFF).contains(&addr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.command {
|
||||||
|
0x0..=0x7 => self.chr_banks[self.command as usize] = value,
|
||||||
|
0x8 => {
|
||||||
|
self.low_bank = value & 0x3F;
|
||||||
|
self.low_is_ram = (value & 0x40) != 0;
|
||||||
|
self.low_ram_enabled = (value & 0x80) != 0;
|
||||||
|
}
|
||||||
|
0x9 => self.prg_banks[0] = value & 0x3F,
|
||||||
|
0xA => self.prg_banks[1] = value & 0x3F,
|
||||||
|
0xB => self.prg_banks[2] = value & 0x3F,
|
||||||
|
0xC => {
|
||||||
|
self.mirroring = match value & 0x03 {
|
||||||
|
0 => Mirroring::Vertical,
|
||||||
|
1 => Mirroring::Horizontal,
|
||||||
|
2 => Mirroring::OneScreenLow,
|
||||||
|
_ => Mirroring::OneScreenHigh,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
0xD => {
|
||||||
|
self.irq_enabled = (value & 0x01) != 0;
|
||||||
|
self.irq_counter_enabled = (value & 0x80) != 0;
|
||||||
|
if !self.irq_enabled {
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xE => {
|
||||||
|
self.irq_counter = (self.irq_counter & 0xFF00) | value as u16;
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
0xF => {
|
||||||
|
self.irq_counter = (self.irq_counter & 0x00FF) | ((value as u16) << 8);
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self.low_is_ram && self.low_ram_enabled {
|
||||||
|
return Some(self.low_ram[self.low_ram_index(addr)]);
|
||||||
|
}
|
||||||
|
if self.low_is_ram {
|
||||||
|
return Some(0);
|
||||||
|
}
|
||||||
|
Some(read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
self.low_bank as usize,
|
||||||
|
(addr as usize) & 0x1FFF,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if !(0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.low_is_ram && self.low_ram_enabled {
|
||||||
|
let idx = self.low_ram_index(addr);
|
||||||
|
self.low_ram[idx] = value;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_banks[page] as usize;
|
||||||
|
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_banks[page] as usize;
|
||||||
|
let total_banks = (self.chr_data.len() / 0x0400).max(1);
|
||||||
|
let bank_idx = safe_mod(bank, total_banks);
|
||||||
|
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
if !self.irq_counter_enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for _ in 0..cycles {
|
||||||
|
if self.irq_counter == 0 {
|
||||||
|
self.irq_counter = 0xFFFF;
|
||||||
|
if self.irq_enabled {
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.command);
|
||||||
|
out.extend_from_slice(&self.chr_banks);
|
||||||
|
out.extend_from_slice(&self.prg_banks);
|
||||||
|
out.push(self.low_bank);
|
||||||
|
out.push(u8::from(self.low_is_ram));
|
||||||
|
out.push(u8::from(self.low_ram_enabled));
|
||||||
|
out.extend_from_slice(&self.irq_counter.to_le_bytes());
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_counter_enabled));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
write_state_bytes(out, &self.low_ram);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 21 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
self.command = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.chr_banks.copy_from_slice(&data[cursor..cursor + 8]);
|
||||||
|
cursor += 8;
|
||||||
|
self.prg_banks.copy_from_slice(&data[cursor..cursor + 3]);
|
||||||
|
cursor += 3;
|
||||||
|
self.low_bank = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.low_is_ram = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.low_ram_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_counter = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||||
|
cursor += 2;
|
||||||
|
self.irq_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_counter_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_pending = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.mirroring = decode_mirroring(data[cursor]);
|
||||||
|
cursor += 1;
|
||||||
|
let low_ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if low_ram_payload.len() != self.low_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.low_ram.copy_from_slice(low_ram_payload);
|
||||||
|
load_chr_state(&mut self.chr_data, &data[cursor..])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
268
src/native_core/mapper/mappers/mapper64.rs
Normal file
268
src/native_core/mapper/mappers/mapper64.rs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper64 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
bank_select: u8,
|
||||||
|
bank_regs: [u8; 16], // R0..RF
|
||||||
|
irq_latch: u8,
|
||||||
|
irq_counter: u8,
|
||||||
|
irq_reload: bool, // set by $C001; applied on next clock
|
||||||
|
irq_enabled: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
irq_delay: u8, // pending IRQ assert delay in CPU cycles
|
||||||
|
cycle_prescaler: u8, // CPU-cycle mode clocks every 4 cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper64 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
bank_select: 0,
|
||||||
|
bank_regs: [0; 16],
|
||||||
|
irq_latch: 0,
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_reload: false,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_pending: false,
|
||||||
|
irq_delay: 0,
|
||||||
|
cycle_prescaler: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_count_1k(&self) -> usize {
|
||||||
|
(self.chr_data.len() / 0x0400).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_mode(&self) -> bool {
|
||||||
|
(self.bank_select & 0x40) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_invert(&self) -> bool {
|
||||||
|
(self.bank_select & 0x80) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_full_1k_mode(&self) -> bool {
|
||||||
|
(self.bank_select & 0x20) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn irq_cycle_mode(&self) -> bool {
|
||||||
|
(self.bank_select & 0x01) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||||
|
let last = self.prg_bank_count_8k().saturating_sub(1);
|
||||||
|
let r6 = self.bank_regs[0x6] as usize;
|
||||||
|
let r7 = self.bank_regs[0x7] as usize;
|
||||||
|
let rf = self.bank_regs[0xF] as usize;
|
||||||
|
match (self.prg_mode(), slot) {
|
||||||
|
(false, 0) => r6,
|
||||||
|
(false, 1) => r7,
|
||||||
|
(false, 2) => rf,
|
||||||
|
(false, 3) => last,
|
||||||
|
(true, 0) => rf,
|
||||||
|
(true, 1) => r7,
|
||||||
|
(true, 2) => r6,
|
||||||
|
(true, 3) => last,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn chr_bank_for_page(&self, page: usize) -> usize {
|
||||||
|
let r = &self.bank_regs;
|
||||||
|
let k = self.chr_full_1k_mode();
|
||||||
|
let r0_lo = (r[0x0] & !1) as usize;
|
||||||
|
let r1_lo = (r[0x1] & !1) as usize;
|
||||||
|
|
||||||
|
let mut layout = [0usize; 8];
|
||||||
|
layout[0] = r0_lo;
|
||||||
|
layout[1] = if k { r[0x8] as usize } else { r0_lo + 1 };
|
||||||
|
layout[2] = r1_lo;
|
||||||
|
layout[3] = if k { r[0x9] as usize } else { r1_lo + 1 };
|
||||||
|
layout[4] = r[0x2] as usize;
|
||||||
|
layout[5] = r[0x3] as usize;
|
||||||
|
layout[6] = r[0x4] as usize;
|
||||||
|
layout[7] = r[0x5] as usize;
|
||||||
|
|
||||||
|
if self.chr_invert() {
|
||||||
|
layout.rotate_left(4);
|
||||||
|
}
|
||||||
|
layout[page]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_counter(&mut self) {
|
||||||
|
if self.irq_reload {
|
||||||
|
self.irq_counter = self.irq_latch;
|
||||||
|
if self.irq_counter != 0 {
|
||||||
|
self.irq_counter |= 1;
|
||||||
|
}
|
||||||
|
self.irq_reload = false;
|
||||||
|
} else if self.irq_counter == 0 {
|
||||||
|
self.irq_counter = self.irq_latch;
|
||||||
|
} else {
|
||||||
|
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.irq_enabled && self.irq_counter == 0 {
|
||||||
|
self.irq_delay = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick_irq_delay(&mut self, cycles: u8) {
|
||||||
|
if self.irq_delay == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if cycles >= self.irq_delay {
|
||||||
|
self.irq_delay = 0;
|
||||||
|
self.irq_pending = true;
|
||||||
|
} else {
|
||||||
|
self.irq_delay -= cycles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper64 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let bank = self.prg_bank_for_slot(slot);
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value,
|
||||||
|
0x8000..=0x9FFF => {
|
||||||
|
let reg = (self.bank_select & 0x0F) as usize;
|
||||||
|
match reg {
|
||||||
|
0x0..=0x9 | 0xF => self.bank_regs[reg] = value,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xA000..=0xBFFF if (addr & 1) == 0 => {
|
||||||
|
self.mirroring = if (value & 1) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
0xC000..=0xDFFF if (addr & 1) == 0 => self.irq_latch = value,
|
||||||
|
0xC000..=0xDFFF => {
|
||||||
|
self.irq_counter = 0;
|
||||||
|
self.irq_reload = true;
|
||||||
|
self.cycle_prescaler = 0;
|
||||||
|
}
|
||||||
|
0xE000..=0xFFFF if (addr & 1) == 0 => {
|
||||||
|
self.irq_enabled = false;
|
||||||
|
self.irq_pending = false;
|
||||||
|
self.irq_delay = 0;
|
||||||
|
}
|
||||||
|
0xE000..=0xFFFF => self.irq_enabled = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_page(page);
|
||||||
|
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_page(page);
|
||||||
|
let total = self.chr_bank_count_1k();
|
||||||
|
let idx = safe_mod(bank, total) * 0x0400 + ((addr as usize) & 0x03FF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
self.tick_irq_delay(cycles);
|
||||||
|
if !self.irq_cycle_mode() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for _ in 0..cycles {
|
||||||
|
self.cycle_prescaler = self.cycle_prescaler.wrapping_add(1);
|
||||||
|
if self.cycle_prescaler >= 4 {
|
||||||
|
self.cycle_prescaler = 0;
|
||||||
|
self.clock_counter();
|
||||||
|
}
|
||||||
|
self.tick_irq_delay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
if !self.irq_cycle_mode() {
|
||||||
|
self.clock_counter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
!self.irq_cycle_mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.bank_select);
|
||||||
|
out.extend_from_slice(&self.bank_regs);
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
out.push(self.irq_latch);
|
||||||
|
out.push(self.irq_counter);
|
||||||
|
out.push(u8::from(self.irq_reload));
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.push(self.irq_delay);
|
||||||
|
out.push(self.cycle_prescaler);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 25 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_select = data[0];
|
||||||
|
self.bank_regs.copy_from_slice(&data[1..17]);
|
||||||
|
self.mirroring = decode_mirroring(data[17]);
|
||||||
|
self.irq_latch = data[18];
|
||||||
|
self.irq_counter = data[19];
|
||||||
|
self.irq_reload = data[20] != 0;
|
||||||
|
self.irq_enabled = data[21] != 0;
|
||||||
|
self.irq_pending = data[22] != 0;
|
||||||
|
self.irq_delay = data[23];
|
||||||
|
self.cycle_prescaler = data[24];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[25..])
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/native_core/mapper/mappers/mapper78.rs
Normal file
123
src/native_core/mapper/mappers/mapper78.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper78 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
submapper: u8,
|
||||||
|
hv_mirroring_mode: bool,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank: u8,
|
||||||
|
mirror_select: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper78 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let hv_mirroring_mode = match rom.header.submapper {
|
||||||
|
1 => false, // Uchuusen / Cosmo Carrier: 1scA/1scB
|
||||||
|
3 => true, // Holy Diver: H/V
|
||||||
|
_ => matches!(rom.header.mirroring, Mirroring::Vertical),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
submapper: rom.header.submapper,
|
||||||
|
hv_mirroring_mode,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank: 0,
|
||||||
|
mirror_select: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper78 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
(self.prg_rom.len() / 0x4000).saturating_sub(1),
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let latched = value & self.cpu_read(addr); // Mapper 78 has bus conflicts.
|
||||||
|
self.prg_bank = latched & 0x07;
|
||||||
|
self.mirror_select = (latched & 0x08) != 0;
|
||||||
|
self.chr_bank = (latched >> 4) & 0x0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let idx = safe_mod(self.chr_bank as usize, total) * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
if self.hv_mirroring_mode {
|
||||||
|
if self.mirror_select {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
}
|
||||||
|
} else if self.mirror_select {
|
||||||
|
Mirroring::OneScreenHigh
|
||||||
|
} else {
|
||||||
|
Mirroring::OneScreenLow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
out.push(self.mirror_select as u8);
|
||||||
|
out.push(self.submapper);
|
||||||
|
out.push(self.hv_mirroring_mode as u8);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 5 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank = data[1];
|
||||||
|
self.mirror_select = (data[2] & 1) != 0;
|
||||||
|
self.submapper = data[3];
|
||||||
|
self.hv_mirroring_mode = (data[4] & 1) != 0;
|
||||||
|
load_chr_state(&mut self.chr_data, &data[5..])
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/native_core/mapper/mappers/mapper87.rs
Normal file
87
src/native_core/mapper/mappers/mapper87.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper87 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
chr_bank: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper87 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
chr_bank: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper87 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
((addr - 0x8000) as usize) / 0x4000,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
let low = value & 0x01;
|
||||||
|
let high = (value >> 1) & 0x01;
|
||||||
|
self.chr_bank = (low << 1) | high;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let idx = safe_mod(self.chr_bank as usize, total_banks) * 0x2000 + (addr as usize & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.chr_bank = data[0];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[1..])
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/native_core/mapper/mappers/mapper88.rs
Normal file
64
src/native_core/mapper/mappers/mapper88.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper88 {
|
||||||
|
base: InesMapper206,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper88 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
base: InesMapper206::new(rom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper88 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.base.cpu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.base.cpu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let mut bank = self.base.chr_bank_for_1k_page(page) as usize;
|
||||||
|
if (addr & 0x1000) != 0 {
|
||||||
|
bank |= 0x40;
|
||||||
|
}
|
||||||
|
read_bank(&self.base.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.base.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let mut bank = self.base.chr_bank_for_1k_page(page) as usize;
|
||||||
|
if (addr & 0x1000) != 0 {
|
||||||
|
bank |= 0x40;
|
||||||
|
}
|
||||||
|
let total_banks = (self.base.chr_data.len() / 0x0400).max(1);
|
||||||
|
let bank_idx = safe_mod(bank, total_banks);
|
||||||
|
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||||
|
if let Some(cell) = self.base.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.base.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
self.base.save_state(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
self.base.load_state(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/native_core/mapper/mappers/mapper93.rs
Normal file
87
src/native_core/mapper/mappers/mapper93.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper93 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper93 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_banks_16k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper93 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_banks_16k().saturating_sub(1),
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.prg_bank = (value >> 4) & 0x07;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[1..])
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/native_core/mapper/mappers/mapper95.rs
Normal file
55
src/native_core/mapper/mappers/mapper95.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct InesMapper95 {
|
||||||
|
base: InesMapper206,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InesMapper95 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
base: InesMapper206::new(rom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for InesMapper95 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.base.cpu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.base.cpu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
self.base.ppu_read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
self.base.ppu_write(addr, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_nametable_addr(&self, addr: u16) -> Option<usize> {
|
||||||
|
if !(0x2000..=0x3EFF).contains(&addr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rel = (addr - 0x2000) & 0x0FFF;
|
||||||
|
let page = (rel / 0x0400) as usize;
|
||||||
|
let offset = (rel & 0x03FF) as usize;
|
||||||
|
let bank = self.base.chr_bank_for_1k_page(page);
|
||||||
|
let ciram_page = ((bank >> 5) & 1) as usize;
|
||||||
|
Some(ciram_page * 0x0400 + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
self.base.save_state(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
self.base.load_state(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
192
src/native_core/mapper/mappers/mmc1.rs
Normal file
192
src/native_core/mapper/mappers/mmc1.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Mmc1 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
shift_reg: u8,
|
||||||
|
shift_count: u8,
|
||||||
|
control: u8,
|
||||||
|
chr_bank0: u8,
|
||||||
|
chr_bank1: u8,
|
||||||
|
prg_bank: u8,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mmc1 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
shift_reg: 0,
|
||||||
|
shift_count: 0,
|
||||||
|
control: 0x0C,
|
||||||
|
chr_bank0: 0,
|
||||||
|
chr_bank1: 0,
|
||||||
|
prg_bank: 0,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_mode(&self) -> u8 {
|
||||||
|
(self.control >> 2) & 0x03
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_mode(&self) -> u8 {
|
||||||
|
(self.control >> 4) & 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_serial(&mut self, addr: u16, value: u8) {
|
||||||
|
if (value & 0x80) != 0 {
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
self.control |= 0x0C;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shift_reg |= (value & 1) << self.shift_count;
|
||||||
|
self.shift_count = self.shift_count.wrapping_add(1);
|
||||||
|
if self.shift_count < 5 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reg = (addr >> 13) & 0x03;
|
||||||
|
match reg {
|
||||||
|
0 => self.control = self.shift_reg & 0x1F,
|
||||||
|
1 => self.chr_bank0 = self.shift_reg & 0x1F,
|
||||||
|
2 => self.chr_bank1 = self.shift_reg & 0x1F,
|
||||||
|
_ => self.prg_bank = self.shift_reg & 0x0F,
|
||||||
|
}
|
||||||
|
self.shift_reg = 0;
|
||||||
|
self.shift_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Mmc1 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prg_mode = self.prg_mode();
|
||||||
|
if prg_mode <= 1 {
|
||||||
|
let bank32 = (self.prg_bank as usize) >> 1;
|
||||||
|
read_bank(&self.prg_rom, 0x8000, bank32, (addr as usize) - 0x8000)
|
||||||
|
} else if prg_mode == 2 {
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(&self.prg_rom, 0x4000, 0, (addr as usize) - 0x8000)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank_count() - 1,
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.write_serial(addr, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if self.chr_mode() == 0 {
|
||||||
|
let bank = (self.chr_bank0 as usize) >> 1;
|
||||||
|
read_bank(&self.chr_data, 0x2000, bank, addr as usize)
|
||||||
|
} else if addr < 0x1000 {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank0 as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank1 as usize,
|
||||||
|
(addr as usize) - 0x1000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let idx = if self.chr_mode() == 0 {
|
||||||
|
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let bank = safe_mod((self.chr_bank0 as usize) >> 1, total_banks);
|
||||||
|
bank * 0x2000 + ((addr as usize) & 0x1FFF)
|
||||||
|
} else {
|
||||||
|
let total_banks = (self.chr_data.len() / 0x1000).max(1);
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank0 as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank1 as usize
|
||||||
|
};
|
||||||
|
safe_mod(bank, total_banks) * 0x1000 + ((addr as usize) & 0x0FFF)
|
||||||
|
};
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match self.control & 0x03 {
|
||||||
|
0 => Mirroring::OneScreenLow,
|
||||||
|
1 => Mirroring::OneScreenHigh,
|
||||||
|
2 => Mirroring::Vertical,
|
||||||
|
3 => Mirroring::Horizontal,
|
||||||
|
_ => self.mirroring_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.shift_reg);
|
||||||
|
out.push(self.shift_count);
|
||||||
|
out.push(self.control);
|
||||||
|
out.push(self.chr_bank0);
|
||||||
|
out.push(self.chr_bank1);
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 6 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.shift_reg = data[0];
|
||||||
|
self.shift_count = data[1];
|
||||||
|
self.control = data[2];
|
||||||
|
self.chr_bank0 = data[3];
|
||||||
|
self.chr_bank1 = data[4];
|
||||||
|
self.prg_bank = data[5];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[6..])
|
||||||
|
}
|
||||||
|
}
|
||||||
178
src/native_core/mapper/mappers/mmc2.rs
Normal file
178
src/native_core/mapper/mappers/mmc2.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Mmc2Latch {
|
||||||
|
Fd,
|
||||||
|
Fe,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Mmc2 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank_8k: u8,
|
||||||
|
chr_fd_0000: u8,
|
||||||
|
chr_fe_0000: u8,
|
||||||
|
chr_fd_1000: u8,
|
||||||
|
chr_fe_1000: u8,
|
||||||
|
latch_0000: Cell<Mmc2Latch>,
|
||||||
|
latch_1000: Cell<Mmc2Latch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mmc2 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank_8k: 0,
|
||||||
|
chr_fd_0000: 0,
|
||||||
|
chr_fe_0000: 0,
|
||||||
|
chr_fd_1000: 0,
|
||||||
|
chr_fe_1000: 0,
|
||||||
|
latch_0000: Cell::new(Mmc2Latch::Fd),
|
||||||
|
latch_1000: Cell::new(Mmc2Latch::Fd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_0000(&self) -> usize {
|
||||||
|
match self.latch_0000.get() {
|
||||||
|
Mmc2Latch::Fd => self.chr_fd_0000 as usize,
|
||||||
|
Mmc2Latch::Fe => self.chr_fe_0000 as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_1000(&self) -> usize {
|
||||||
|
match self.latch_1000.get() {
|
||||||
|
Mmc2Latch::Fd => self.chr_fd_1000 as usize,
|
||||||
|
Mmc2Latch::Fe => self.chr_fe_1000 as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_latches_for_ppu_addr(&self, addr: u16) {
|
||||||
|
if (0x0FD8..=0x0FDF).contains(&addr) {
|
||||||
|
self.latch_0000.set(Mmc2Latch::Fd);
|
||||||
|
} else if (0x0FE8..=0x0FEF).contains(&addr) {
|
||||||
|
self.latch_0000.set(Mmc2Latch::Fe);
|
||||||
|
} else if (0x1FD8..=0x1FDF).contains(&addr) {
|
||||||
|
self.latch_1000.set(Mmc2Latch::Fd);
|
||||||
|
} else if (0x1FE8..=0x1FEF).contains(&addr) {
|
||||||
|
self.latch_1000.set(Mmc2Latch::Fe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Mmc2 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let bank = match slot {
|
||||||
|
0 => self.prg_bank_8k as usize,
|
||||||
|
1 => self.prg_bank_count_8k().saturating_sub(3),
|
||||||
|
2 => self.prg_bank_count_8k().saturating_sub(2),
|
||||||
|
_ => self.prg_bank_count_8k().saturating_sub(1),
|
||||||
|
};
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0xA000..=0xAFFF => self.prg_bank_8k = value & 0x0F,
|
||||||
|
0xB000..=0xBFFF => self.chr_fd_0000 = value & 0x1F,
|
||||||
|
0xC000..=0xCFFF => self.chr_fe_0000 = value & 0x1F,
|
||||||
|
0xD000..=0xDFFF => self.chr_fd_1000 = value & 0x1F,
|
||||||
|
0xE000..=0xEFFF => self.chr_fe_1000 = value & 0x1F,
|
||||||
|
0xF000..=0xFFFF => {
|
||||||
|
self.mirroring = if (value & 1) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let value = if addr < 0x1000 {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank_0000(),
|
||||||
|
addr as usize & 0x0FFF,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank_1000(),
|
||||||
|
(addr as usize) & 0x0FFF,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
// MMC2 latches update after the triggering tile fetch.
|
||||||
|
self.update_latches_for_ppu_addr(addr);
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, _value: u8) {
|
||||||
|
self.update_latches_for_ppu_addr(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank_8k);
|
||||||
|
out.push(self.chr_fd_0000);
|
||||||
|
out.push(self.chr_fe_0000);
|
||||||
|
out.push(self.chr_fd_1000);
|
||||||
|
out.push(self.chr_fe_1000);
|
||||||
|
out.push(match self.latch_0000.get() {
|
||||||
|
Mmc2Latch::Fd => 0,
|
||||||
|
Mmc2Latch::Fe => 1,
|
||||||
|
});
|
||||||
|
out.push(match self.latch_1000.get() {
|
||||||
|
Mmc2Latch::Fd => 0,
|
||||||
|
Mmc2Latch::Fe => 1,
|
||||||
|
});
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() != 8 {
|
||||||
|
return Err("mapper state payload has invalid length".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank_8k = data[0];
|
||||||
|
self.chr_fd_0000 = data[1];
|
||||||
|
self.chr_fe_0000 = data[2];
|
||||||
|
self.chr_fd_1000 = data[3];
|
||||||
|
self.chr_fe_1000 = data[4];
|
||||||
|
self.latch_0000.set(if data[5] == 0 {
|
||||||
|
Mmc2Latch::Fd
|
||||||
|
} else {
|
||||||
|
Mmc2Latch::Fe
|
||||||
|
});
|
||||||
|
self.latch_1000.set(if data[6] == 0 {
|
||||||
|
Mmc2Latch::Fd
|
||||||
|
} else {
|
||||||
|
Mmc2Latch::Fe
|
||||||
|
});
|
||||||
|
self.mirroring = decode_mirroring(data[7]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
250
src/native_core/mapper/mappers/mmc3.rs
Normal file
250
src/native_core/mapper/mappers/mmc3.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Mmc3 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
prg_ram_enabled: bool,
|
||||||
|
prg_ram_write_protect: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
bank_regs: [u8; 8],
|
||||||
|
bank_select: u8,
|
||||||
|
irq_latch: u8,
|
||||||
|
irq_counter: u8,
|
||||||
|
irq_reload: bool,
|
||||||
|
irq_enabled: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mmc3 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
prg_ram: vec![0; 0x2000],
|
||||||
|
prg_ram_enabled: true,
|
||||||
|
prg_ram_write_protect: false,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
bank_regs: [0; 8],
|
||||||
|
bank_select: 0,
|
||||||
|
irq_latch: 0,
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_reload: false,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_mode(&self) -> bool {
|
||||||
|
(self.bank_select & 0x40) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_invert(&self) -> bool {
|
||||||
|
(self.bank_select & 0x80) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||||
|
let last = self.prg_bank_count_8k() - 1;
|
||||||
|
let second_last = last.saturating_sub(1);
|
||||||
|
match (self.prg_mode(), slot) {
|
||||||
|
(false, 0) => self.bank_regs[6] as usize,
|
||||||
|
(false, 1) => self.bank_regs[7] as usize,
|
||||||
|
(false, 2) => second_last,
|
||||||
|
(false, 3) => last,
|
||||||
|
(true, 0) => second_last,
|
||||||
|
(true, 1) => self.bank_regs[7] as usize,
|
||||||
|
(true, 2) => self.bank_regs[6] as usize,
|
||||||
|
(true, 3) => last,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn chr_bank_for_1k_page(&self, page: usize) -> usize {
|
||||||
|
let regs = &self.bank_regs;
|
||||||
|
let mut layout = [0usize; 8];
|
||||||
|
|
||||||
|
layout[0] = (regs[0] as usize) & !1;
|
||||||
|
layout[1] = layout[0] + 1;
|
||||||
|
layout[2] = (regs[1] as usize) & !1;
|
||||||
|
layout[3] = layout[2] + 1;
|
||||||
|
layout[4] = regs[2] as usize;
|
||||||
|
layout[5] = regs[3] as usize;
|
||||||
|
layout[6] = regs[4] as usize;
|
||||||
|
layout[7] = regs[5] as usize;
|
||||||
|
|
||||||
|
if self.chr_invert() {
|
||||||
|
layout.rotate_left(4);
|
||||||
|
}
|
||||||
|
layout[page]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_irq_scanline(&mut self) {
|
||||||
|
if self.irq_reload || self.irq_counter == 0 {
|
||||||
|
self.irq_counter = self.irq_latch;
|
||||||
|
self.irq_reload = false;
|
||||||
|
} else {
|
||||||
|
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||||
|
}
|
||||||
|
if self.irq_enabled && self.irq_counter == 0 {
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Mmc3 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let bank = self.prg_bank_for_slot(slot);
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value,
|
||||||
|
0x8000..=0x9FFF => {
|
||||||
|
let reg = (self.bank_select & 0x07) as usize;
|
||||||
|
self.bank_regs[reg] = value;
|
||||||
|
}
|
||||||
|
0xA000..=0xBFFF if (addr & 1) == 0 => {
|
||||||
|
self.mirroring = if (value & 1) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
0xA000..=0xBFFF => {
|
||||||
|
self.prg_ram_enabled = (value & 0x80) != 0;
|
||||||
|
self.prg_ram_write_protect = (value & 0x40) != 0;
|
||||||
|
}
|
||||||
|
0xC000..=0xDFFF if (addr & 1) == 0 => self.irq_latch = value,
|
||||||
|
0xC000..=0xDFFF => {
|
||||||
|
self.irq_counter = 0;
|
||||||
|
self.irq_reload = true;
|
||||||
|
}
|
||||||
|
0xE000..=0xFFFF if (addr & 1) == 0 => {
|
||||||
|
self.irq_enabled = false;
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
0xE000..=0xFFFF => self.irq_enabled = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
if self.prg_ram_enabled {
|
||||||
|
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||||
|
} else {
|
||||||
|
Some(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
if self.prg_ram_enabled && !self.prg_ram_write_protect {
|
||||||
|
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_1k_page(page);
|
||||||
|
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_1k_page(page);
|
||||||
|
let total_banks = (self.chr_data.len() / 0x0400).max(1);
|
||||||
|
let bank_idx = safe_mod(bank, total_banks);
|
||||||
|
let idx = bank_idx * 0x0400 + ((addr as usize) & 0x03FF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
self.clock_irq_scanline();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.bank_regs);
|
||||||
|
out.push(self.bank_select);
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
out.push(self.irq_latch);
|
||||||
|
out.push(self.irq_counter);
|
||||||
|
out.push(u8::from(self.irq_reload));
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.push(u8::from(self.prg_ram_enabled));
|
||||||
|
out.push(u8::from(self.prg_ram_write_protect));
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 17 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_regs.copy_from_slice(&data[0..8]);
|
||||||
|
self.bank_select = data[8];
|
||||||
|
self.mirroring = decode_mirroring(data[9]);
|
||||||
|
self.irq_latch = data[10];
|
||||||
|
self.irq_counter = data[11];
|
||||||
|
self.irq_reload = data[12] != 0;
|
||||||
|
self.irq_enabled = data[13] != 0;
|
||||||
|
self.irq_pending = data[14] != 0;
|
||||||
|
let mut cursor = 15usize;
|
||||||
|
self.prg_ram_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_ram_write_protect = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if prg_ram.len() != self.prg_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.prg_ram.copy_from_slice(prg_ram);
|
||||||
|
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src/native_core/mapper/mappers/mmc4.rs
Normal file
180
src/native_core/mapper/mappers/mmc4.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Mmc2Latch {
|
||||||
|
Fd,
|
||||||
|
Fe,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Mmc4 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank_16k: u8,
|
||||||
|
chr_fd_0000: u8,
|
||||||
|
chr_fe_0000: u8,
|
||||||
|
chr_fd_1000: u8,
|
||||||
|
chr_fe_1000: u8,
|
||||||
|
latch_0000: Cell<Mmc2Latch>,
|
||||||
|
latch_1000: Cell<Mmc2Latch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mmc4 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank_16k: 0,
|
||||||
|
chr_fd_0000: 0,
|
||||||
|
chr_fe_0000: 0,
|
||||||
|
chr_fd_1000: 0,
|
||||||
|
chr_fe_1000: 0,
|
||||||
|
latch_0000: Cell::new(Mmc2Latch::Fd),
|
||||||
|
latch_1000: Cell::new(Mmc2Latch::Fd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_16k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_0000(&self) -> usize {
|
||||||
|
match self.latch_0000.get() {
|
||||||
|
Mmc2Latch::Fd => self.chr_fd_0000 as usize,
|
||||||
|
Mmc2Latch::Fe => self.chr_fe_0000 as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_1000(&self) -> usize {
|
||||||
|
match self.latch_1000.get() {
|
||||||
|
Mmc2Latch::Fd => self.chr_fd_1000 as usize,
|
||||||
|
Mmc2Latch::Fe => self.chr_fe_1000 as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_latches_for_ppu_addr(&self, addr: u16) {
|
||||||
|
if (0x0FD8..=0x0FDF).contains(&addr) {
|
||||||
|
self.latch_0000.set(Mmc2Latch::Fd);
|
||||||
|
} else if (0x0FE8..=0x0FEF).contains(&addr) {
|
||||||
|
self.latch_0000.set(Mmc2Latch::Fe);
|
||||||
|
} else if (0x1FD8..=0x1FDF).contains(&addr) {
|
||||||
|
self.latch_1000.set(Mmc2Latch::Fd);
|
||||||
|
} else if (0x1FE8..=0x1FEF).contains(&addr) {
|
||||||
|
self.latch_1000.set(Mmc2Latch::Fe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Mmc4 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank_16k as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank_count_16k().saturating_sub(1),
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0xA000..=0xAFFF => self.prg_bank_16k = value & 0x0F,
|
||||||
|
0xB000..=0xBFFF => self.chr_fd_0000 = value & 0x1F,
|
||||||
|
0xC000..=0xCFFF => self.chr_fe_0000 = value & 0x1F,
|
||||||
|
0xD000..=0xDFFF => self.chr_fd_1000 = value & 0x1F,
|
||||||
|
0xE000..=0xEFFF => self.chr_fe_1000 = value & 0x1F,
|
||||||
|
0xF000..=0xFFFF => {
|
||||||
|
self.mirroring = if (value & 1) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let value = if addr < 0x1000 {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank_0000(),
|
||||||
|
addr as usize & 0x0FFF,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x1000,
|
||||||
|
self.chr_bank_1000(),
|
||||||
|
(addr as usize) & 0x0FFF,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
// MMC4 latches update after the triggering tile fetch.
|
||||||
|
self.update_latches_for_ppu_addr(addr);
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, _value: u8) {
|
||||||
|
self.update_latches_for_ppu_addr(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank_16k);
|
||||||
|
out.push(self.chr_fd_0000);
|
||||||
|
out.push(self.chr_fe_0000);
|
||||||
|
out.push(self.chr_fd_1000);
|
||||||
|
out.push(self.chr_fe_1000);
|
||||||
|
out.push(match self.latch_0000.get() {
|
||||||
|
Mmc2Latch::Fd => 0,
|
||||||
|
Mmc2Latch::Fe => 1,
|
||||||
|
});
|
||||||
|
out.push(match self.latch_1000.get() {
|
||||||
|
Mmc2Latch::Fd => 0,
|
||||||
|
Mmc2Latch::Fe => 1,
|
||||||
|
});
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() != 8 {
|
||||||
|
return Err("mapper state payload has invalid length".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank_16k = data[0];
|
||||||
|
self.chr_fd_0000 = data[1];
|
||||||
|
self.chr_fe_0000 = data[2];
|
||||||
|
self.chr_fd_1000 = data[3];
|
||||||
|
self.chr_fe_1000 = data[4];
|
||||||
|
self.latch_0000.set(if data[5] == 0 {
|
||||||
|
Mmc2Latch::Fd
|
||||||
|
} else {
|
||||||
|
Mmc2Latch::Fe
|
||||||
|
});
|
||||||
|
self.latch_1000.set(if data[6] == 0 {
|
||||||
|
Mmc2Latch::Fd
|
||||||
|
} else {
|
||||||
|
Mmc2Latch::Fe
|
||||||
|
});
|
||||||
|
self.mirroring = decode_mirroring(data[7]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
359
src/native_core/mapper/mappers/mmc5.rs
Normal file
359
src/native_core/mapper/mappers/mmc5.rs
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Mmc5 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
prg_mode: u8,
|
||||||
|
chr_mode: u8,
|
||||||
|
prg_regs_8k: [u8; 4],
|
||||||
|
prg_ram_bank: u8,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
chr_banks_1k: [u16; 8],
|
||||||
|
nt_mapping: u8,
|
||||||
|
ram_protect_1: u8,
|
||||||
|
ram_protect_2: u8,
|
||||||
|
chr_upper_bits: u8,
|
||||||
|
multiplier_a: u8,
|
||||||
|
multiplier_b: u8,
|
||||||
|
irq_scanline: u8,
|
||||||
|
irq_enable: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
irq_cycles: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mmc5 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let prg_banks_8k = (rom.prg_rom.len() / 0x2000).max(1);
|
||||||
|
let last = prg_banks_8k.saturating_sub(1) as u8;
|
||||||
|
let second_last = prg_banks_8k.saturating_sub(2) as u8;
|
||||||
|
let mut chr_banks_1k = [0u16; 8];
|
||||||
|
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||||
|
*bank = i as u16;
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
prg_mode: 3,
|
||||||
|
chr_mode: 3,
|
||||||
|
prg_regs_8k: [0, 1, second_last, last],
|
||||||
|
prg_ram_bank: 0,
|
||||||
|
prg_ram: vec![0; 0x10000],
|
||||||
|
chr_banks_1k,
|
||||||
|
nt_mapping: 0x50,
|
||||||
|
ram_protect_1: 0,
|
||||||
|
ram_protect_2: 0,
|
||||||
|
chr_upper_bits: 0,
|
||||||
|
multiplier_a: 0,
|
||||||
|
multiplier_b: 0,
|
||||||
|
irq_scanline: 0,
|
||||||
|
irq_enable: false,
|
||||||
|
irq_pending: false,
|
||||||
|
irq_cycles: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_count_1k(&self) -> usize {
|
||||||
|
(self.chr_data.len() / 0x0400).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_8k_for_slot(&self, slot: usize) -> usize {
|
||||||
|
let regs = &self.prg_regs_8k;
|
||||||
|
match self.prg_mode & 0x03 {
|
||||||
|
0 => {
|
||||||
|
let bank32 = (regs[3] as usize) >> 2;
|
||||||
|
bank32 * 4 + slot
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let bank16_lo = (regs[1] as usize) >> 1;
|
||||||
|
let bank16_hi = (regs[3] as usize) >> 1;
|
||||||
|
if slot < 2 {
|
||||||
|
bank16_lo * 2 + slot
|
||||||
|
} else {
|
||||||
|
bank16_hi * 2 + (slot - 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => match slot {
|
||||||
|
0 | 1 => {
|
||||||
|
let bank16 = (regs[1] as usize) >> 1;
|
||||||
|
bank16 * 2 + slot
|
||||||
|
}
|
||||||
|
2 => regs[2] as usize,
|
||||||
|
_ => regs[3] as usize,
|
||||||
|
},
|
||||||
|
_ => regs[slot] as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_1k_for_page(&self, page: usize) -> usize {
|
||||||
|
let mut banks = [0usize; 8];
|
||||||
|
match self.chr_mode & 0x03 {
|
||||||
|
0 => {
|
||||||
|
let base = (self.chr_banks_1k[7] as usize) & !7;
|
||||||
|
for (i, bank) in banks.iter_mut().enumerate() {
|
||||||
|
*bank = base + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let b0 = (self.chr_banks_1k[3] as usize) & !3;
|
||||||
|
let b1 = (self.chr_banks_1k[7] as usize) & !3;
|
||||||
|
for i in 0..4usize {
|
||||||
|
banks[i] = b0 + i;
|
||||||
|
banks[i + 4] = b1 + i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let b0 = (self.chr_banks_1k[1] as usize) & !1;
|
||||||
|
let b1 = (self.chr_banks_1k[3] as usize) & !1;
|
||||||
|
let b2 = (self.chr_banks_1k[5] as usize) & !1;
|
||||||
|
let b3 = (self.chr_banks_1k[7] as usize) & !1;
|
||||||
|
banks[0] = b0;
|
||||||
|
banks[1] = b0 + 1;
|
||||||
|
banks[2] = b1;
|
||||||
|
banks[3] = b1 + 1;
|
||||||
|
banks[4] = b2;
|
||||||
|
banks[5] = b2 + 1;
|
||||||
|
banks[6] = b3;
|
||||||
|
banks[7] = b3 + 1;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
for (i, bank) in banks.iter_mut().enumerate() {
|
||||||
|
*bank = self.chr_banks_1k[i] as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
banks[page]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ram_writable(&self) -> bool {
|
||||||
|
self.ram_protect_1 == 0x02 && self.ram_protect_2 == 0x01
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_mirroring(&self) -> Mirroring {
|
||||||
|
let nt0 = self.nt_mapping & 0x03;
|
||||||
|
let nt1 = (self.nt_mapping >> 2) & 0x03;
|
||||||
|
let nt2 = (self.nt_mapping >> 4) & 0x03;
|
||||||
|
let nt3 = (self.nt_mapping >> 6) & 0x03;
|
||||||
|
if nt0 == 0 && nt1 == 1 && nt2 == 0 && nt3 == 1 {
|
||||||
|
return Mirroring::Vertical;
|
||||||
|
}
|
||||||
|
if nt0 == 0 && nt1 == 0 && nt2 == 1 && nt3 == 1 {
|
||||||
|
return Mirroring::Horizontal;
|
||||||
|
}
|
||||||
|
if nt0 == 0 && nt1 == 0 && nt2 == 0 && nt3 == 0 {
|
||||||
|
return Mirroring::OneScreenLow;
|
||||||
|
}
|
||||||
|
if nt0 == 1 && nt1 == 1 && nt2 == 1 && nt3 == 1 {
|
||||||
|
return Mirroring::OneScreenHigh;
|
||||||
|
}
|
||||||
|
self.mirroring_default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Mmc5 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let bank = self.prg_bank_8k_for_slot(slot);
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
match addr {
|
||||||
|
0x5204 => {
|
||||||
|
let mut status = 0u8;
|
||||||
|
if self.irq_pending {
|
||||||
|
status |= 0x80;
|
||||||
|
}
|
||||||
|
if self.irq_enable {
|
||||||
|
status |= 0x40;
|
||||||
|
}
|
||||||
|
Some(status)
|
||||||
|
}
|
||||||
|
0x5205 => Some(((self.multiplier_a as u16 * self.multiplier_b as u16) & 0x00FF) as u8),
|
||||||
|
0x5206 => Some(((self.multiplier_a as u16 * self.multiplier_b as u16) >> 8) as u8),
|
||||||
|
0x6000..=0x7FFF => {
|
||||||
|
let bank = (self.prg_ram_bank & 0x07) as usize;
|
||||||
|
let idx = bank * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||||
|
Some(self.prg_ram.get(idx).copied().unwrap_or(0))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
match addr {
|
||||||
|
0x5100 => self.prg_mode = value & 0x03,
|
||||||
|
0x5101 => self.chr_mode = value & 0x03,
|
||||||
|
0x5102 => self.ram_protect_1 = value & 0x03,
|
||||||
|
0x5103 => self.ram_protect_2 = value & 0x03,
|
||||||
|
0x5105 => self.nt_mapping = value,
|
||||||
|
0x5113 => self.prg_ram_bank = value & 0x07,
|
||||||
|
0x5114..=0x5117 => {
|
||||||
|
let reg = (addr - 0x5114) as usize;
|
||||||
|
self.prg_regs_8k[reg] = value & 0x7F;
|
||||||
|
}
|
||||||
|
0x5120..=0x5127 => {
|
||||||
|
let reg = (addr - 0x5120) as usize;
|
||||||
|
let bank = (((self.chr_upper_bits & 0x03) as u16) << 8) | value as u16;
|
||||||
|
self.chr_banks_1k[reg] = bank;
|
||||||
|
}
|
||||||
|
0x5128..=0x512B => {
|
||||||
|
let reg = (addr - 0x5128) as usize;
|
||||||
|
let bank = (((self.chr_upper_bits & 0x03) as u16) << 8) | value as u16;
|
||||||
|
let base = reg * 2;
|
||||||
|
self.chr_banks_1k[base] = bank & !1;
|
||||||
|
self.chr_banks_1k[base + 1] = (bank & !1).wrapping_add(1);
|
||||||
|
}
|
||||||
|
0x5130 => self.chr_upper_bits = value & 0x03,
|
||||||
|
0x5203 => self.irq_scanline = value,
|
||||||
|
0x5204 => {
|
||||||
|
self.irq_enable = (value & 0x80) != 0;
|
||||||
|
if !self.irq_enable {
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x5205 => self.multiplier_a = value,
|
||||||
|
0x5206 => self.multiplier_b = value,
|
||||||
|
0x6000..=0x7FFF => {
|
||||||
|
if self.ram_writable() {
|
||||||
|
let bank = (self.prg_ram_bank & 0x07) as usize;
|
||||||
|
let idx = bank * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||||
|
if let Some(cell) = self.prg_ram.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = safe_mod(self.chr_bank_1k_for_page(page), self.chr_bank_count_1k());
|
||||||
|
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = safe_mod(self.chr_bank_1k_for_page(page), self.chr_bank_count_1k());
|
||||||
|
let idx = bank * 0x0400 + ((addr as usize) & 0x03FF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.decode_mirroring()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
if !self.irq_enable {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.irq_cycles = self.irq_cycles.saturating_add(cycles as u32);
|
||||||
|
let threshold = (self.irq_scanline as u32 + 1).saturating_mul(113);
|
||||||
|
if threshold != 0 && self.irq_cycles >= threshold {
|
||||||
|
self.irq_cycles %= threshold;
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_mode);
|
||||||
|
out.push(self.chr_mode);
|
||||||
|
out.extend_from_slice(&self.prg_regs_8k);
|
||||||
|
out.push(self.prg_ram_bank);
|
||||||
|
out.push(self.nt_mapping);
|
||||||
|
out.push(self.ram_protect_1);
|
||||||
|
out.push(self.ram_protect_2);
|
||||||
|
out.push(self.chr_upper_bits);
|
||||||
|
out.push(self.multiplier_a);
|
||||||
|
out.push(self.multiplier_b);
|
||||||
|
out.push(self.irq_scanline);
|
||||||
|
out.push(u8::from(self.irq_enable));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.extend_from_slice(&self.irq_cycles.to_le_bytes());
|
||||||
|
for bank in self.chr_banks_1k {
|
||||||
|
out.extend_from_slice(&bank.to_le_bytes());
|
||||||
|
}
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 23 + 16 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
self.prg_mode = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.chr_mode = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_regs_8k.copy_from_slice(&data[cursor..cursor + 4]);
|
||||||
|
cursor += 4;
|
||||||
|
self.prg_ram_bank = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.nt_mapping = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.ram_protect_1 = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.ram_protect_2 = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.chr_upper_bits = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.multiplier_a = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.multiplier_b = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_scanline = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_enable = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_pending = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_cycles = u32::from_le_bytes([
|
||||||
|
data[cursor],
|
||||||
|
data[cursor + 1],
|
||||||
|
data[cursor + 2],
|
||||||
|
data[cursor + 3],
|
||||||
|
]);
|
||||||
|
cursor += 4;
|
||||||
|
for i in 0..8usize {
|
||||||
|
self.chr_banks_1k[i] = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||||
|
cursor += 2;
|
||||||
|
}
|
||||||
|
let prg_ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if prg_ram_payload.len() != self.prg_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.prg_ram.copy_from_slice(prg_ram_payload);
|
||||||
|
load_chr_state(&mut self.chr_data, &data[cursor..])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/native_core/mapper/mappers/mod.rs
Normal file
84
src/native_core/mapper/mappers/mod.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use super::core::*;
|
||||||
|
use super::types::*;
|
||||||
|
use crate::native_core::ines::{InesRom, Mirroring};
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
mod axrom;
|
||||||
|
mod bandai70_152;
|
||||||
|
mod bnrom34;
|
||||||
|
mod camerica71;
|
||||||
|
mod cnrom;
|
||||||
|
mod color_dreams11;
|
||||||
|
mod cprom13;
|
||||||
|
mod crazy_climber180;
|
||||||
|
mod fme7;
|
||||||
|
mod gxrom;
|
||||||
|
mod mapper105;
|
||||||
|
mod mapper118;
|
||||||
|
mod mapper140;
|
||||||
|
mod mapper155;
|
||||||
|
mod mapper158;
|
||||||
|
mod mapper184;
|
||||||
|
mod mapper185;
|
||||||
|
mod mapper206;
|
||||||
|
mod mapper206_submapper1;
|
||||||
|
mod mapper253;
|
||||||
|
mod mapper64;
|
||||||
|
mod mapper78;
|
||||||
|
mod mapper87;
|
||||||
|
mod mapper88;
|
||||||
|
mod mapper93;
|
||||||
|
mod mapper95;
|
||||||
|
mod mmc1;
|
||||||
|
mod mmc2;
|
||||||
|
mod mmc3;
|
||||||
|
mod mmc4;
|
||||||
|
mod mmc5;
|
||||||
|
mod namco163_19;
|
||||||
|
mod nina79;
|
||||||
|
mod nrom;
|
||||||
|
mod tqrom119;
|
||||||
|
mod un1rom94;
|
||||||
|
mod unrom512_30;
|
||||||
|
mod uxrom;
|
||||||
|
mod vrc;
|
||||||
|
|
||||||
|
pub(super) use axrom::Axrom;
|
||||||
|
pub(super) use bandai70_152::Bandai70_152;
|
||||||
|
pub(super) use bnrom34::Bnrom34;
|
||||||
|
pub(super) use camerica71::Camerica71;
|
||||||
|
pub(super) use cnrom::Cnrom;
|
||||||
|
pub(super) use color_dreams11::ColorDreams11;
|
||||||
|
pub(super) use cprom13::Cprom13;
|
||||||
|
pub(super) use crazy_climber180::CrazyClimber180;
|
||||||
|
pub(super) use fme7::Fme7;
|
||||||
|
pub(super) use gxrom::Gxrom;
|
||||||
|
pub(super) use mapper64::InesMapper64;
|
||||||
|
pub(super) use mapper78::InesMapper78;
|
||||||
|
pub(super) use mapper87::InesMapper87;
|
||||||
|
pub(super) use mapper88::InesMapper88;
|
||||||
|
pub(super) use mapper93::InesMapper93;
|
||||||
|
pub(super) use mapper95::InesMapper95;
|
||||||
|
pub(super) use mapper105::InesMapper105;
|
||||||
|
pub(super) use mapper118::InesMapper118;
|
||||||
|
pub(super) use mapper140::InesMapper140;
|
||||||
|
pub(super) use mapper155::InesMapper155;
|
||||||
|
pub(super) use mapper158::InesMapper158;
|
||||||
|
pub(super) use mapper184::InesMapper184;
|
||||||
|
pub(super) use mapper185::InesMapper185;
|
||||||
|
pub(super) use mapper206::InesMapper206;
|
||||||
|
pub(super) use mapper206_submapper1::InesMapper206Submapper1;
|
||||||
|
pub(super) use mapper253::InesMapper253;
|
||||||
|
pub(super) use mmc1::Mmc1;
|
||||||
|
pub(super) use mmc2::Mmc2;
|
||||||
|
pub(super) use mmc3::Mmc3;
|
||||||
|
pub(super) use mmc4::Mmc4;
|
||||||
|
pub(super) use mmc5::Mmc5;
|
||||||
|
pub(super) use namco163_19::Namco163_19;
|
||||||
|
pub(super) use nina79::Nina79;
|
||||||
|
pub(super) use nrom::Nrom;
|
||||||
|
pub(super) use tqrom119::Tqrom119;
|
||||||
|
pub(super) use un1rom94::Un1rom94;
|
||||||
|
pub(super) use unrom512_30::Unrom512_30;
|
||||||
|
pub(super) use uxrom::Uxrom;
|
||||||
|
pub(super) use vrc::{Vrc1_75, Vrc2_23, Vrc6_24, Vrc7_85};
|
||||||
202
src/native_core/mapper/mappers/namco163_19.rs
Normal file
202
src/native_core/mapper/mappers/namco163_19.rs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Namco163_19 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
prg_banks_8k: [u8; 3],
|
||||||
|
chr_banks_1k: [u8; 8],
|
||||||
|
audio_ram: [u8; 0x80],
|
||||||
|
irq_counter: u16,
|
||||||
|
irq_enabled: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Namco163_19 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_ram: vec![0; 0x2000],
|
||||||
|
prg_banks_8k: [0, 1, 2],
|
||||||
|
chr_banks_1k: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||||
|
audio_ram: [0; 0x80],
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_count_1k(&self) -> usize {
|
||||||
|
(self.chr_data.len() / 0x0400).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Namco163_19 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let bank = match slot {
|
||||||
|
0 => self.prg_banks_8k[0] as usize,
|
||||||
|
1 => self.prg_banks_8k[1] as usize,
|
||||||
|
2 => self.prg_banks_8k[2] as usize,
|
||||||
|
_ => self.prg_bank_count_8k().saturating_sub(1),
|
||||||
|
};
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0xDFFF => {
|
||||||
|
let reg = ((addr - 0x8000) / 0x0800) as usize;
|
||||||
|
if reg < 8 {
|
||||||
|
self.chr_banks_1k[reg] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xE000..=0xE7FF => self.prg_banks_8k[0] = value & 0x3F,
|
||||||
|
0xE800..=0xEFFF => self.prg_banks_8k[1] = value & 0x3F,
|
||||||
|
0xF000..=0xF7FF => self.prg_banks_8k[2] = value & 0x3F,
|
||||||
|
0xF800..=0xFFFF => {
|
||||||
|
self.mirroring = if (value & 1) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
match addr {
|
||||||
|
0x4800..=0x487F => Some(self.audio_ram[(addr as usize) & 0x7F]),
|
||||||
|
0x5000 => Some((self.irq_counter & 0x00FF) as u8),
|
||||||
|
0x5800 => {
|
||||||
|
let mut hi = ((self.irq_counter >> 8) as u8) & 0x7F;
|
||||||
|
if self.irq_enabled {
|
||||||
|
hi |= 0x80;
|
||||||
|
}
|
||||||
|
Some(hi)
|
||||||
|
}
|
||||||
|
0x6000..=0x7FFF => Some(self.prg_ram[(addr as usize) - 0x6000]),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
match addr {
|
||||||
|
0x4800..=0x487F => self.audio_ram[(addr as usize) & 0x7F] = value,
|
||||||
|
0x5000 => {
|
||||||
|
self.irq_counter = (self.irq_counter & 0xFF00) | value as u16;
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
0x5800 => {
|
||||||
|
self.irq_counter = (self.irq_counter & 0x00FF) | (((value & 0x7F) as u16) << 8);
|
||||||
|
self.irq_enabled = (value & 0x80) != 0;
|
||||||
|
if !self.irq_enabled {
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x6000..=0x7FFF => self.prg_ram[(addr as usize) - 0x6000] = value,
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||||
|
read_bank(&self.chr_data, 0x0400, bank, (addr as usize) & 0x03FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||||
|
let idx = bank * 0x0400 + ((addr as usize) & 0x03FF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
if !self.irq_enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let sum = self.irq_counter as u32 + cycles as u32;
|
||||||
|
if sum > 0x7FFF {
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
self.irq_counter = (sum as u16) & 0x7FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
out.extend_from_slice(&self.prg_banks_8k);
|
||||||
|
out.extend_from_slice(&self.chr_banks_1k);
|
||||||
|
out.extend_from_slice(&self.audio_ram);
|
||||||
|
out.extend_from_slice(&self.irq_counter.to_le_bytes());
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 1 + 3 + 8 + 0x80 + 4 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
self.mirroring = decode_mirroring(data[cursor]);
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_banks_8k.copy_from_slice(&data[cursor..cursor + 3]);
|
||||||
|
cursor += 3;
|
||||||
|
self.chr_banks_1k.copy_from_slice(&data[cursor..cursor + 8]);
|
||||||
|
cursor += 8;
|
||||||
|
self.audio_ram.copy_from_slice(&data[cursor..cursor + 0x80]);
|
||||||
|
cursor += 0x80;
|
||||||
|
self.irq_counter = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||||
|
cursor += 2;
|
||||||
|
self.irq_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_pending = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
let prg_ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if prg_ram_payload.len() != self.prg_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.prg_ram.copy_from_slice(prg_ram_payload);
|
||||||
|
load_chr_state(&mut self.chr_data, &data[cursor..])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/native_core/mapper/mappers/nina79.rs
Normal file
91
src/native_core/mapper/mappers/nina79.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Nina79 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Nina79 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Nina79 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x8000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
// NINA-003/006 latch: 010x xxx1 xxxx xxxx ($4100/$4300/.../$5F00).
|
||||||
|
if (addr & 0xE100) == 0x4100 {
|
||||||
|
self.prg_bank = (value >> 3) & 0x01;
|
||||||
|
self.chr_bank = value & 0x07;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let idx = safe_mod(self.chr_bank as usize, total_banks) * 0x2000 + (addr as usize & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank = data[1];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[2..])
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/native_core/mapper/mappers/nrom.rs
Normal file
73
src/native_core/mapper/mappers/nrom.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Nrom {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Nrom {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Nrom {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
((addr - 0x8000) as usize) / 0x4000,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, _addr: u16, _value: u8) {}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
let _ = addr;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, _value: u8) -> bool {
|
||||||
|
let _ = addr;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
load_chr_state(&mut self.chr_data, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/native_core/mapper/mappers/tqrom119.rs
Normal file
269
src/native_core/mapper/mappers/tqrom119.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Tqrom119 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_rom: Vec<u8>,
|
||||||
|
chr_ram: Vec<u8>,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
prg_ram_enabled: bool,
|
||||||
|
prg_ram_write_protect: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
bank_regs: [u8; 8],
|
||||||
|
bank_select: u8,
|
||||||
|
irq_latch: u8,
|
||||||
|
irq_counter: u8,
|
||||||
|
irq_reload: bool,
|
||||||
|
irq_enabled: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tqrom119 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_rom: rom.chr_data,
|
||||||
|
chr_ram: vec![0; 0x2000],
|
||||||
|
prg_ram: vec![0; 0x2000],
|
||||||
|
prg_ram_enabled: true,
|
||||||
|
prg_ram_write_protect: false,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
bank_regs: [0; 8],
|
||||||
|
bank_select: 0,
|
||||||
|
irq_latch: 0,
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_reload: false,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_mode(&self) -> bool {
|
||||||
|
(self.bank_select & 0x40) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_invert(&self) -> bool {
|
||||||
|
(self.bank_select & 0x80) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_for_slot(&self, slot: usize) -> usize {
|
||||||
|
let last = self.prg_bank_count_8k() - 1;
|
||||||
|
let second_last = last.saturating_sub(1);
|
||||||
|
match (self.prg_mode(), slot) {
|
||||||
|
(false, 0) => self.bank_regs[6] as usize,
|
||||||
|
(false, 1) => self.bank_regs[7] as usize,
|
||||||
|
(false, 2) => second_last,
|
||||||
|
(false, 3) => last,
|
||||||
|
(true, 0) => second_last,
|
||||||
|
(true, 1) => self.bank_regs[7] as usize,
|
||||||
|
(true, 2) => self.bank_regs[6] as usize,
|
||||||
|
(true, 3) => last,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_for_1k_page(&self, page: usize) -> u8 {
|
||||||
|
let regs = &self.bank_regs;
|
||||||
|
let mut layout = [0u8; 8];
|
||||||
|
|
||||||
|
layout[0] = regs[0] & !1;
|
||||||
|
layout[1] = layout[0].wrapping_add(1);
|
||||||
|
layout[2] = regs[1] & !1;
|
||||||
|
layout[3] = layout[2].wrapping_add(1);
|
||||||
|
layout[4] = regs[2];
|
||||||
|
layout[5] = regs[3];
|
||||||
|
layout[6] = regs[4];
|
||||||
|
layout[7] = regs[5];
|
||||||
|
|
||||||
|
if self.chr_invert() {
|
||||||
|
layout.rotate_left(4);
|
||||||
|
}
|
||||||
|
layout[page]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_irq_scanline(&mut self) {
|
||||||
|
if self.irq_reload || self.irq_counter == 0 {
|
||||||
|
self.irq_counter = self.irq_latch;
|
||||||
|
self.irq_reload = false;
|
||||||
|
} else {
|
||||||
|
self.irq_counter = self.irq_counter.wrapping_sub(1);
|
||||||
|
}
|
||||||
|
if self.irq_enabled && self.irq_counter == 0 {
|
||||||
|
self.irq_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_read_page(&self, bank: u8, offset: usize) -> u8 {
|
||||||
|
if (bank & 0x40) != 0 {
|
||||||
|
read_bank(&self.chr_ram, 0x0400, (bank & 0x07) as usize, offset)
|
||||||
|
} else {
|
||||||
|
read_bank(&self.chr_rom, 0x0400, (bank & 0x3F) as usize, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_write_page(&mut self, bank: u8, offset: usize, value: u8) {
|
||||||
|
if (bank & 0x40) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (bank & 0x07) as usize;
|
||||||
|
let idx = page * 0x0400 + (offset & 0x03FF);
|
||||||
|
if let Some(cell) = self.chr_ram.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Tqrom119 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let bank = self.prg_bank_for_slot(slot);
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x2000,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & 0x1FFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0x9FFF if (addr & 1) == 0 => self.bank_select = value,
|
||||||
|
0x8000..=0x9FFF => {
|
||||||
|
let reg = (self.bank_select & 0x07) as usize;
|
||||||
|
self.bank_regs[reg] = value;
|
||||||
|
}
|
||||||
|
0xA000..=0xBFFF if (addr & 1) == 0 => {
|
||||||
|
self.mirroring = if (value & 1) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
0xA000..=0xBFFF => {
|
||||||
|
self.prg_ram_enabled = (value & 0x80) != 0;
|
||||||
|
self.prg_ram_write_protect = (value & 0x40) != 0;
|
||||||
|
}
|
||||||
|
0xC000..=0xDFFF if (addr & 1) == 0 => self.irq_latch = value,
|
||||||
|
0xC000..=0xDFFF => {
|
||||||
|
self.irq_counter = 0;
|
||||||
|
self.irq_reload = true;
|
||||||
|
}
|
||||||
|
0xE000..=0xFFFF if (addr & 1) == 0 => {
|
||||||
|
self.irq_enabled = false;
|
||||||
|
self.irq_pending = false;
|
||||||
|
}
|
||||||
|
0xE000..=0xFFFF => self.irq_enabled = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
if self.prg_ram_enabled {
|
||||||
|
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||||
|
} else {
|
||||||
|
Some(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
if self.prg_ram_enabled && !self.prg_ram_write_protect {
|
||||||
|
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_1k_page(page);
|
||||||
|
self.chr_read_page(bank, (addr as usize) & 0x03FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / 0x0400) as usize;
|
||||||
|
let bank = self.chr_bank_for_1k_page(page);
|
||||||
|
self.chr_write_page(bank, (addr as usize) & 0x03FF, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_scanline(&mut self) {
|
||||||
|
self.clock_irq_scanline();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_ppu_a12_clock(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.bank_regs);
|
||||||
|
out.push(self.bank_select);
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
out.push(self.irq_latch);
|
||||||
|
out.push(self.irq_counter);
|
||||||
|
out.push(u8::from(self.irq_reload));
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.push(u8::from(self.prg_ram_enabled));
|
||||||
|
out.push(u8::from(self.prg_ram_write_protect));
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
write_state_bytes(out, &self.chr_ram);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 17 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_regs.copy_from_slice(&data[0..8]);
|
||||||
|
self.bank_select = data[8];
|
||||||
|
self.mirroring = decode_mirroring(data[9]);
|
||||||
|
self.irq_latch = data[10];
|
||||||
|
self.irq_counter = data[11];
|
||||||
|
self.irq_reload = data[12] != 0;
|
||||||
|
self.irq_enabled = data[13] != 0;
|
||||||
|
self.irq_pending = data[14] != 0;
|
||||||
|
let mut cursor = 15usize;
|
||||||
|
self.prg_ram_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_ram_write_protect = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if prg_ram.len() != self.prg_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.prg_ram.copy_from_slice(prg_ram);
|
||||||
|
let chr_ram = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if chr_ram.len() != self.chr_ram.len() || cursor != data.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.chr_ram.copy_from_slice(chr_ram);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/native_core/mapper/mappers/un1rom94.rs
Normal file
87
src/native_core/mapper/mappers/un1rom94.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Un1rom94 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
bank_select: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Un1rom94 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
bank_select: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_banks(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Un1rom94 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.bank_select as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_banks() - 1,
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.bank_select = (value >> 2) & 0x07;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.bank_select);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_select = data[0];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[1..])
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/native_core/mapper/mappers/unrom512_30.rs
Normal file
123
src/native_core/mapper/mappers/unrom512_30.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Unrom512_30 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
prg_bank: u8,
|
||||||
|
chr_bank: u8,
|
||||||
|
one_screen_hi: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Unrom512_30 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let mut chr_data = rom.chr_data;
|
||||||
|
if rom.chr_is_ram && chr_data.len() < 0x8000 {
|
||||||
|
chr_data.resize(0x8000, 0);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
prg_bank: 0,
|
||||||
|
chr_bank: 0,
|
||||||
|
one_screen_hi: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_16k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Unrom512_30 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_bank_count_16k().saturating_sub(1),
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.prg_bank = value & 0x1F;
|
||||||
|
self.chr_bank = (value >> 5) & 0x03;
|
||||||
|
self.one_screen_hi = Some((value & 0x80) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
0x2000,
|
||||||
|
self.chr_bank as usize,
|
||||||
|
addr as usize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let total_banks = (self.chr_data.len() / 0x2000).max(1);
|
||||||
|
let bank_idx = safe_mod(self.chr_bank as usize, total_banks);
|
||||||
|
let idx = bank_idx * 0x2000 + ((addr as usize) & 0x1FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match self.one_screen_hi {
|
||||||
|
Some(true) => Mirroring::OneScreenHigh,
|
||||||
|
Some(false) => Mirroring::OneScreenLow,
|
||||||
|
None => self.mirroring_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank);
|
||||||
|
out.push(self.chr_bank);
|
||||||
|
out.push(match self.one_screen_hi {
|
||||||
|
Some(true) => 2,
|
||||||
|
Some(false) => 1,
|
||||||
|
None => 0,
|
||||||
|
});
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank = data[0];
|
||||||
|
self.chr_bank = data[1];
|
||||||
|
self.one_screen_hi = match data[2] {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(false),
|
||||||
|
2 => Some(true),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
load_chr_state(&mut self.chr_data, &data[3..])
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/native_core/mapper/mappers/uxrom.rs
Normal file
87
src/native_core/mapper/mappers/uxrom.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Uxrom {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
bank_select: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Uxrom {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
bank_select: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_banks(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / 0x4000).max(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Uxrom {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.bank_select as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
0x4000,
|
||||||
|
self.prg_banks() - 1,
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr >= 0x8000 {
|
||||||
|
self.bank_select = value & 0x0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.chr_data.get(addr as usize).copied().unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(addr as usize) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.bank_select);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.bank_select = data[0];
|
||||||
|
load_chr_state(&mut self.chr_data, &data[1..])
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/native_core/mapper/mappers/vrc/mod.rs
Normal file
11
src/native_core/mapper/mappers/vrc/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod vrc1_75;
|
||||||
|
mod vrc2_23;
|
||||||
|
mod vrc6_24;
|
||||||
|
mod vrc7_85;
|
||||||
|
|
||||||
|
pub(crate) use vrc1_75::Vrc1_75;
|
||||||
|
pub(crate) use vrc2_23::Vrc2_23;
|
||||||
|
pub(crate) use vrc6_24::Vrc6_24;
|
||||||
|
pub(crate) use vrc7_85::Vrc7_85;
|
||||||
122
src/native_core/mapper/mappers/vrc/vrc1_75.rs
Normal file
122
src/native_core/mapper/mappers/vrc/vrc1_75.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Vrc1_75 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
prg_bank_8k_8000: u8,
|
||||||
|
prg_bank_8k_a000: u8,
|
||||||
|
prg_bank_8k_c000: u8,
|
||||||
|
chr_bank_0: u8,
|
||||||
|
chr_bank_1: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vrc1_75 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
prg_bank_8k_8000: 0,
|
||||||
|
prg_bank_8k_a000: 1,
|
||||||
|
prg_bank_8k_c000: 2,
|
||||||
|
chr_bank_0: 0,
|
||||||
|
chr_bank_1: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Vrc1_75 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0xA000 {
|
||||||
|
self.prg_bank_8k_8000 as usize
|
||||||
|
} else if addr < 0xC000 {
|
||||||
|
self.prg_bank_8k_a000 as usize
|
||||||
|
} else if addr < 0xE000 {
|
||||||
|
self.prg_bank_8k_c000 as usize
|
||||||
|
} else {
|
||||||
|
(self.prg_rom.len() / 0x2000).max(1).saturating_sub(1)
|
||||||
|
};
|
||||||
|
read_bank(&self.prg_rom, 0x2000, bank, (addr as usize) & 0x1FFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0x8FFF => self.prg_bank_8k_8000 = value & 0x0F,
|
||||||
|
0x9000..=0x9FFF => {
|
||||||
|
self.mirroring = if (value & 1) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
self.chr_bank_0 = (self.chr_bank_0 & 0x0F) | ((value & 0x02) << 3);
|
||||||
|
self.chr_bank_1 = (self.chr_bank_1 & 0x0F) | ((value & 0x04) << 2);
|
||||||
|
}
|
||||||
|
0xA000..=0xAFFF => self.prg_bank_8k_a000 = value & 0x0F,
|
||||||
|
0xC000..=0xCFFF => self.prg_bank_8k_c000 = value & 0x0F,
|
||||||
|
0xE000..=0xEFFF => self.chr_bank_0 = (self.chr_bank_0 & 0x10) | (value & 0x0F),
|
||||||
|
0xF000..=0xFFFF => self.chr_bank_1 = (self.chr_bank_1 & 0x10) | (value & 0x0F),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank_0 as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_1 as usize
|
||||||
|
};
|
||||||
|
read_bank(&self.chr_data, 0x1000, bank, (addr as usize) & 0x0FFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bank = if addr < 0x1000 {
|
||||||
|
self.chr_bank_0 as usize
|
||||||
|
} else {
|
||||||
|
self.chr_bank_1 as usize
|
||||||
|
};
|
||||||
|
let total = (self.chr_data.len() / 0x1000).max(1);
|
||||||
|
let idx = safe_mod(bank, total) * 0x1000 + ((addr as usize) & 0x0FFF);
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank_8k_8000);
|
||||||
|
out.push(self.prg_bank_8k_a000);
|
||||||
|
out.push(self.prg_bank_8k_c000);
|
||||||
|
out.push(self.chr_bank_0);
|
||||||
|
out.push(self.chr_bank_1);
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 6 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
self.prg_bank_8k_8000 = data[0];
|
||||||
|
self.prg_bank_8k_a000 = data[1];
|
||||||
|
self.prg_bank_8k_c000 = data[2];
|
||||||
|
self.chr_bank_0 = data[3];
|
||||||
|
self.chr_bank_1 = data[4];
|
||||||
|
self.mirroring = decode_mirroring(data[5]);
|
||||||
|
load_chr_state(&mut self.chr_data, &data[6..])
|
||||||
|
}
|
||||||
|
}
|
||||||
399
src/native_core/mapper/mappers/vrc/vrc2_23.rs
Normal file
399
src/native_core/mapper/mappers/vrc/vrc2_23.rs
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Vrc2_23 {
|
||||||
|
pub(crate) mapper_id: u16,
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
mirroring: Mirroring,
|
||||||
|
has_irq: bool,
|
||||||
|
submapper: u8,
|
||||||
|
prg_swap: bool,
|
||||||
|
prg_bank_8000: u8,
|
||||||
|
prg_bank_a000: u8,
|
||||||
|
chr_banks_1k: [u16; 8],
|
||||||
|
irq_latch: u8,
|
||||||
|
irq_counter: u8,
|
||||||
|
irq_enabled: bool,
|
||||||
|
irq_enabled_after_ack: bool,
|
||||||
|
irq_mode_cpu: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
irq_prescaler: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vrc2_23 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let mapper_id = rom.header.mapper;
|
||||||
|
let submapper = rom.header.submapper;
|
||||||
|
let has_irq = mapper_id != 22 && !matches!(submapper, 3 | 4);
|
||||||
|
let mut chr_banks_1k = [0u16; 8];
|
||||||
|
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||||
|
*bank = i as u16;
|
||||||
|
}
|
||||||
|
let prg_ram_size = if rom.header.prg_ram_shift > 0 {
|
||||||
|
64usize << rom.header.prg_ram_shift
|
||||||
|
} else {
|
||||||
|
8192 // Default 8KB WRAM for VRC2/VRC4 boards
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
mapper_id,
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
prg_ram: vec![0; prg_ram_size],
|
||||||
|
mirroring: rom.header.mirroring,
|
||||||
|
has_irq,
|
||||||
|
submapper,
|
||||||
|
prg_swap: false,
|
||||||
|
prg_bank_8000: 0,
|
||||||
|
prg_bank_a000: 1,
|
||||||
|
chr_banks_1k,
|
||||||
|
irq_latch: 0,
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_enabled_after_ack: false,
|
||||||
|
irq_mode_cpu: false,
|
||||||
|
irq_pending: false,
|
||||||
|
irq_prescaler: 341,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_with_submapper(rom: InesRom, forced_submapper: u8) -> Self {
|
||||||
|
let mut mapper = Self::new(rom);
|
||||||
|
mapper.submapper = forced_submapper;
|
||||||
|
mapper
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_count_1k(&self) -> usize {
|
||||||
|
(self.chr_data.len() / CHR_BANK_1K).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_chr_nibble(&mut self, bank: usize, high: bool, value: u8) {
|
||||||
|
if bank >= 8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cur = self.chr_banks_1k[bank];
|
||||||
|
self.chr_banks_1k[bank] = if high {
|
||||||
|
(cur & 0x000F) | (((value & 0x0F) as u16) << 4)
|
||||||
|
} else {
|
||||||
|
(cur & 0x01F0) | ((value & 0x0F) as u16)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_subindex(addr: u16, a0_bit: u8, a1_bit: u8) -> usize {
|
||||||
|
let a0 = ((addr >> a0_bit) & 1) as usize;
|
||||||
|
let a1 = ((addr >> a1_bit) & 1) as usize;
|
||||||
|
a0 | (a1 << 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_wiring_bits(&self) -> (u8, u8) {
|
||||||
|
match self.mapper_id {
|
||||||
|
21 => (1, 2), // VRC4a-style
|
||||||
|
22 => (1, 0), // VRC2a (swapped A0/A1)
|
||||||
|
23 => (0, 1), // VRC2b/VRC4f
|
||||||
|
25 => (1, 0), // VRC2c/VRC4d (swapped)
|
||||||
|
_ => (0, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn higher_wiring_bits(&self) -> (u8, u8) {
|
||||||
|
match self.mapper_id {
|
||||||
|
21 => (6, 7), // VRC4c-style
|
||||||
|
22 => (1, 0), // VRC2a has a single decode variant
|
||||||
|
23 => (2, 3), // VRC4e
|
||||||
|
25 => (3, 2), // VRC4d (swapped)
|
||||||
|
_ => (0, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_subindices(&self, addr: u16) -> ([usize; 2], usize) {
|
||||||
|
if self.mapper_id == 22 {
|
||||||
|
let (a0, a1) = self.lower_wiring_bits();
|
||||||
|
return ([Self::decode_subindex(addr, a0, a1), 0], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (lower_a0, lower_a1) = self.lower_wiring_bits();
|
||||||
|
let lower = Self::decode_subindex(addr, lower_a0, lower_a1);
|
||||||
|
match self.submapper {
|
||||||
|
1 | 3 => ([lower, 0], 1), // lower-variant wiring only
|
||||||
|
2 | 4 => {
|
||||||
|
let (higher_a0, higher_a1) = self.higher_wiring_bits();
|
||||||
|
([Self::decode_subindex(addr, higher_a0, higher_a1), 0], 1)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let (higher_a0, higher_a1) = self.higher_wiring_bits();
|
||||||
|
// iNES and NES2 submapper 0: support both known address families,
|
||||||
|
// but choose one decode per write to avoid conflicting double-updates.
|
||||||
|
let higher_selected =
|
||||||
|
((addr >> higher_a0) & 1) != 0 || ((addr >> higher_a1) & 1) != 0;
|
||||||
|
if higher_selected {
|
||||||
|
([Self::decode_subindex(addr, higher_a0, higher_a1), 0], 1)
|
||||||
|
} else {
|
||||||
|
([lower, 0], 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_chr_bank(&self, raw_bank: u16) -> usize {
|
||||||
|
if self.mapper_id == 22 {
|
||||||
|
// VRC2a (mapper 22): CHR bank value is shifted right by 1.
|
||||||
|
(raw_bank >> 1) as usize
|
||||||
|
} else {
|
||||||
|
raw_bank as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn irq_state(&mut self) -> VrcIrqRegisters<'_> {
|
||||||
|
VrcIrqRegisters {
|
||||||
|
latch: &mut self.irq_latch,
|
||||||
|
counter: &mut self.irq_counter,
|
||||||
|
enabled: &mut self.irq_enabled,
|
||||||
|
enabled_after_ack: &mut self.irq_enabled_after_ack,
|
||||||
|
mode_cpu: &mut self.irq_mode_cpu,
|
||||||
|
pending: &mut self.irq_pending,
|
||||||
|
prescaler: &mut self.irq_prescaler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Vrc2_23 {
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) && !self.prg_ram.is_empty() {
|
||||||
|
Some(self.prg_ram[(addr as usize - 0x6000) % self.prg_ram.len()])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) && !self.prg_ram.is_empty() {
|
||||||
|
let idx = (addr as usize - 0x6000) % self.prg_ram.len();
|
||||||
|
self.prg_ram[idx] = value;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let last = (self.prg_rom.len() / PRG_BANK_8K).max(1).saturating_sub(1);
|
||||||
|
let fixed_second_last = last.saturating_sub(1);
|
||||||
|
let bank0 = if self.has_irq && self.prg_swap {
|
||||||
|
fixed_second_last
|
||||||
|
} else {
|
||||||
|
self.prg_bank_8000 as usize
|
||||||
|
};
|
||||||
|
let bank2 = if self.has_irq && self.prg_swap {
|
||||||
|
self.prg_bank_8000 as usize
|
||||||
|
} else {
|
||||||
|
fixed_second_last
|
||||||
|
};
|
||||||
|
let bank = match slot {
|
||||||
|
0 => bank0,
|
||||||
|
1 => self.prg_bank_a000 as usize,
|
||||||
|
2 => bank2,
|
||||||
|
_ => last,
|
||||||
|
};
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
PRG_BANK_8K,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & (PRG_BANK_8K - 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x8000..=0x8FFF => self.prg_bank_8000 = value & 0x1F,
|
||||||
|
0x9000..=0x9FFF => {
|
||||||
|
let (subs, count) = self.decode_subindices(addr);
|
||||||
|
for i in 0..count {
|
||||||
|
let sub = subs[i];
|
||||||
|
if i > 0 && sub == subs[0] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if sub == 0 {
|
||||||
|
self.mirroring = if self.has_irq {
|
||||||
|
match value & 0x03 {
|
||||||
|
0 => Mirroring::Vertical,
|
||||||
|
1 => Mirroring::Horizontal,
|
||||||
|
2 => Mirroring::OneScreenLow,
|
||||||
|
_ => Mirroring::OneScreenHigh,
|
||||||
|
}
|
||||||
|
} else if (value & 0x01) == 0 {
|
||||||
|
Mirroring::Vertical
|
||||||
|
} else {
|
||||||
|
Mirroring::Horizontal
|
||||||
|
};
|
||||||
|
} else if sub == 2 && self.has_irq {
|
||||||
|
self.prg_swap = (value & 0x02) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xA000..=0xAFFF => self.prg_bank_a000 = value & 0x1F,
|
||||||
|
0xB000..=0xEFFF => {
|
||||||
|
let group = ((addr - 0xB000) / 0x1000) as usize;
|
||||||
|
let (subs, count) = self.decode_subindices(addr);
|
||||||
|
for i in 0..count {
|
||||||
|
let sub = subs[i];
|
||||||
|
if i > 0 && sub == subs[0] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let bank = group * 2 + ((sub >> 1) & 1);
|
||||||
|
let high = (sub & 1) != 0;
|
||||||
|
self.set_chr_nibble(bank, high, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xF000..=0xFFFF if self.has_irq => {
|
||||||
|
let (subs, count) = self.decode_subindices(addr);
|
||||||
|
for i in 0..count {
|
||||||
|
let sub = subs[i];
|
||||||
|
if i > 0 && sub == subs[0] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match sub {
|
||||||
|
0 => self.irq_latch = (self.irq_latch & 0xF0) | (value & 0x0F),
|
||||||
|
1 => self.irq_latch = (self.irq_latch & 0x0F) | ((value & 0x0F) << 4),
|
||||||
|
2 => vrc_irq_write_control(value, self.irq_state()),
|
||||||
|
_ => vrc_irq_ack(self.irq_state()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||||
|
let bank = safe_mod(
|
||||||
|
self.effective_chr_bank(self.chr_banks_1k[page]),
|
||||||
|
self.chr_bank_count_1k(),
|
||||||
|
);
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
CHR_BANK_1K,
|
||||||
|
bank,
|
||||||
|
(addr as usize) & (CHR_BANK_1K - 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||||
|
let bank = safe_mod(
|
||||||
|
self.effective_chr_bank(self.chr_banks_1k[page]),
|
||||||
|
self.chr_bank_count_1k(),
|
||||||
|
);
|
||||||
|
let idx = bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1));
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
if !self.has_irq {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vrc_irq_clock(cycles, self.irq_state());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(self.prg_bank_8000);
|
||||||
|
out.push(self.prg_bank_a000);
|
||||||
|
out.push(encode_mirroring(self.mirroring));
|
||||||
|
out.push(u8::from(self.has_irq));
|
||||||
|
out.push(u8::from(self.chr_is_ram));
|
||||||
|
out.push(self.submapper);
|
||||||
|
out.push(u8::from(self.prg_swap));
|
||||||
|
out.push(self.irq_latch);
|
||||||
|
out.push(self.irq_counter);
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_enabled_after_ack));
|
||||||
|
out.push(u8::from(self.irq_mode_cpu));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
|
||||||
|
for bank in self.chr_banks_1k {
|
||||||
|
out.extend_from_slice(&bank.to_le_bytes());
|
||||||
|
}
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 35 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
self.prg_bank_8000 = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_bank_a000 = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.mirroring = decode_mirroring(data[cursor]);
|
||||||
|
cursor += 1;
|
||||||
|
|
||||||
|
self.has_irq = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.chr_is_ram = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.submapper = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_swap = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_latch = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_counter = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_enabled_after_ack = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_mode_cpu = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_pending = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_prescaler = i16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||||
|
cursor += 2;
|
||||||
|
if data.len().saturating_sub(cursor) < 16 + 4 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
for i in 0..8usize {
|
||||||
|
let off = cursor + i * 2;
|
||||||
|
self.chr_banks_1k[i] = u16::from_le_bytes([data[off], data[off + 1]]);
|
||||||
|
}
|
||||||
|
cursor += 16;
|
||||||
|
let chr_payload = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if chr_payload.len() != self.chr_data.len() {
|
||||||
|
return Err("mapper CHR state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.chr_data.copy_from_slice(chr_payload);
|
||||||
|
// Try to load PRG-RAM if present (backwards compatible)
|
||||||
|
if cursor + 4 <= data.len() {
|
||||||
|
let ram_payload = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if ram_payload.len() == self.prg_ram.len() {
|
||||||
|
self.prg_ram.copy_from_slice(ram_payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/native_core/mapper/mappers/vrc/vrc6_24.rs
Normal file
259
src/native_core/mapper/mappers/vrc/vrc6_24.rs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Vrc6_24 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
mapper26_wiring: bool,
|
||||||
|
prg_bank_16k: u8,
|
||||||
|
prg_bank_8k: u8,
|
||||||
|
chr_banks_1k: [u8; 8],
|
||||||
|
control: u8,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
irq_latch: u8,
|
||||||
|
irq_counter: u8,
|
||||||
|
irq_enabled: bool,
|
||||||
|
irq_enabled_after_ack: bool,
|
||||||
|
irq_mode_cpu: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
irq_prescaler: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vrc6_24 {
|
||||||
|
pub(crate) fn new(rom: InesRom, mapper26_wiring: bool) -> Self {
|
||||||
|
let mut chr_banks_1k = [0u8; 8];
|
||||||
|
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||||
|
*bank = i as u8;
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
mapper26_wiring,
|
||||||
|
prg_bank_16k: 0,
|
||||||
|
prg_bank_8k: 0,
|
||||||
|
chr_banks_1k,
|
||||||
|
control: 0,
|
||||||
|
prg_ram: vec![0; PRG_RAM_8K],
|
||||||
|
irq_latch: 0,
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_enabled_after_ack: false,
|
||||||
|
irq_mode_cpu: false,
|
||||||
|
irq_pending: false,
|
||||||
|
irq_prescaler: 341,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_register(&self, addr: u16) -> u16 {
|
||||||
|
let hi = addr & 0xF000;
|
||||||
|
let mut lo = addr & 0x0003;
|
||||||
|
if self.mapper26_wiring {
|
||||||
|
lo = match lo {
|
||||||
|
1 => 2,
|
||||||
|
2 => 1,
|
||||||
|
_ => lo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
hi | lo
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_page_bank(&self, page: usize) -> usize {
|
||||||
|
let mode = self.control & 0x03;
|
||||||
|
let idx = match mode {
|
||||||
|
0 => page,
|
||||||
|
1 => [0, 1, 1, 3, 2, 5, 3, 7][page],
|
||||||
|
_ => [0, 0, 1, 1, 2, 2, 3, 3][page],
|
||||||
|
};
|
||||||
|
self.chr_banks_1k[idx] as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn irq_state(&mut self) -> VrcIrqRegisters<'_> {
|
||||||
|
VrcIrqRegisters {
|
||||||
|
latch: &mut self.irq_latch,
|
||||||
|
counter: &mut self.irq_counter,
|
||||||
|
enabled: &mut self.irq_enabled,
|
||||||
|
enabled_after_ack: &mut self.irq_enabled_after_ack,
|
||||||
|
mode_cpu: &mut self.irq_mode_cpu,
|
||||||
|
pending: &mut self.irq_pending,
|
||||||
|
prescaler: &mut self.irq_prescaler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Vrc6_24 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if addr < 0xC000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
PRG_BANK_16K,
|
||||||
|
self.prg_bank_16k as usize,
|
||||||
|
(addr as usize) - 0x8000,
|
||||||
|
)
|
||||||
|
} else if addr < 0xE000 {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
PRG_BANK_8K,
|
||||||
|
self.prg_bank_8k as usize,
|
||||||
|
(addr as usize) - 0xC000,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
PRG_BANK_8K,
|
||||||
|
(self.prg_rom.len() / PRG_BANK_8K).max(1).saturating_sub(1),
|
||||||
|
(addr as usize) - 0xE000,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if addr < 0x8000 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match self.decode_register(addr) {
|
||||||
|
0x8000..=0x8003 => self.prg_bank_16k = value & 0x0F,
|
||||||
|
0x9003 => self.control = value,
|
||||||
|
0xC000..=0xC003 => self.prg_bank_8k = value & 0x1F,
|
||||||
|
0xD000 => self.chr_banks_1k[0] = value,
|
||||||
|
0xD001 => self.chr_banks_1k[1] = value,
|
||||||
|
0xD002 => self.chr_banks_1k[2] = value,
|
||||||
|
0xD003 => self.chr_banks_1k[3] = value,
|
||||||
|
0xE000 => self.chr_banks_1k[4] = value,
|
||||||
|
0xE001 => self.chr_banks_1k[5] = value,
|
||||||
|
0xE002 => self.chr_banks_1k[6] = value,
|
||||||
|
0xE003 => self.chr_banks_1k[7] = value,
|
||||||
|
0xF000 => self.irq_latch = value,
|
||||||
|
0xF001 => vrc_irq_write_control(value, self.irq_state()),
|
||||||
|
0xF002 => vrc_irq_ack(self.irq_state()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) && (self.control & 0x80) != 0 {
|
||||||
|
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) && (self.control & 0x80) != 0 {
|
||||||
|
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||||
|
let bank = self.chr_page_bank(page);
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
CHR_BANK_1K,
|
||||||
|
bank,
|
||||||
|
(addr as usize) & (CHR_BANK_1K - 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||||
|
let bank = safe_mod(
|
||||||
|
self.chr_page_bank(page),
|
||||||
|
(self.chr_data.len() / CHR_BANK_1K).max(1),
|
||||||
|
);
|
||||||
|
let idx = bank * CHR_BANK_1K + ((addr as usize) & (CHR_BANK_1K - 1));
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
match (self.control >> 2) & 0x03 {
|
||||||
|
0 => Mirroring::Vertical,
|
||||||
|
1 => Mirroring::Horizontal,
|
||||||
|
2 => Mirroring::OneScreenLow,
|
||||||
|
3 => Mirroring::OneScreenHigh,
|
||||||
|
_ => self.mirroring_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
vrc_irq_clock(cycles, self.irq_state());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.push(u8::from(self.mapper26_wiring));
|
||||||
|
out.push(self.prg_bank_16k);
|
||||||
|
out.push(self.prg_bank_8k);
|
||||||
|
out.extend_from_slice(&self.chr_banks_1k);
|
||||||
|
out.push(self.control);
|
||||||
|
out.push(self.irq_latch);
|
||||||
|
out.push(self.irq_counter);
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_enabled_after_ack));
|
||||||
|
out.push(u8::from(self.irq_mode_cpu));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
|
||||||
|
write_state_bytes(out, &self.prg_ram);
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
if data.len() < 1 + 1 + 1 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 2 {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
self.mapper26_wiring = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_bank_16k = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.prg_bank_8k = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.chr_banks_1k.copy_from_slice(&data[cursor..cursor + 8]);
|
||||||
|
cursor += 8;
|
||||||
|
self.control = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_latch = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_counter = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_enabled_after_ack = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_mode_cpu = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_pending = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
self.irq_prescaler = i16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||||
|
cursor += 2;
|
||||||
|
|
||||||
|
let prg_ram = read_state_bytes(data, &mut cursor)?;
|
||||||
|
if prg_ram.len() != self.prg_ram.len() {
|
||||||
|
return Err("mapper state does not match loaded ROM".to_string());
|
||||||
|
}
|
||||||
|
self.prg_ram.copy_from_slice(prg_ram);
|
||||||
|
|
||||||
|
load_chr_state(&mut self.chr_data, &data[cursor..])
|
||||||
|
}
|
||||||
|
}
|
||||||
309
src/native_core/mapper/mappers/vrc/vrc7_85.rs
Normal file
309
src/native_core/mapper/mappers/vrc/vrc7_85.rs
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) struct Vrc7_85 {
|
||||||
|
prg_rom: Vec<u8>,
|
||||||
|
chr_data: Vec<u8>,
|
||||||
|
chr_is_ram: bool,
|
||||||
|
submapper: u8,
|
||||||
|
mirroring_default: Mirroring,
|
||||||
|
prg_ram: Vec<u8>,
|
||||||
|
prg_ram_enabled: bool,
|
||||||
|
prg_banks_8k: [u8; 3],
|
||||||
|
chr_banks_1k: [u8; 8],
|
||||||
|
irq_latch: u8,
|
||||||
|
irq_counter: u8,
|
||||||
|
irq_enabled: bool,
|
||||||
|
irq_enabled_after_ack: bool,
|
||||||
|
irq_mode_cpu: bool,
|
||||||
|
irq_pending: bool,
|
||||||
|
irq_prescaler: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vrc7_85 {
|
||||||
|
pub(crate) fn new(rom: InesRom) -> Self {
|
||||||
|
let mut chr_banks_1k = [0u8; 8];
|
||||||
|
for (i, bank) in chr_banks_1k.iter_mut().enumerate() {
|
||||||
|
*bank = i as u8;
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
prg_rom: rom.prg_rom,
|
||||||
|
chr_data: rom.chr_data,
|
||||||
|
chr_is_ram: rom.chr_is_ram,
|
||||||
|
submapper: rom.header.submapper,
|
||||||
|
mirroring_default: rom.header.mirroring,
|
||||||
|
prg_ram: vec![0; PRG_RAM_8K],
|
||||||
|
prg_ram_enabled: false,
|
||||||
|
prg_banks_8k: [0, 1, 2],
|
||||||
|
chr_banks_1k,
|
||||||
|
irq_latch: 0,
|
||||||
|
irq_counter: 0,
|
||||||
|
irq_enabled: false,
|
||||||
|
irq_enabled_after_ack: false,
|
||||||
|
irq_mode_cpu: false,
|
||||||
|
irq_pending: false,
|
||||||
|
irq_prescaler: 341,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prg_bank_count_8k(&self) -> usize {
|
||||||
|
(self.prg_rom.len() / PRG_BANK_8K).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chr_bank_count_1k(&self) -> usize {
|
||||||
|
(self.chr_data.len() / CHR_BANK_1K).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_secondary_addr(&self, addr: u16) -> bool {
|
||||||
|
let a3 = (addr & 0x0008) != 0;
|
||||||
|
let a4 = (addr & 0x0010) != 0;
|
||||||
|
match self.submapper {
|
||||||
|
1 => a3,
|
||||||
|
2 => a4,
|
||||||
|
_ => a3 || a4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_chr_reg(&self, addr: u16) -> Option<usize> {
|
||||||
|
let group = ((addr >> 12) & 0x0F) as usize;
|
||||||
|
if !(0x0A..=0x0D).contains(&group) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let base = (group - 0x0A) * 2;
|
||||||
|
let odd = usize::from(self.is_secondary_addr(addr));
|
||||||
|
Some(base + odd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_native_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
let min_len = PRG_RAM_8K
|
||||||
|
+ 1 // submapper
|
||||||
|
+ 1 // prg_ram_enabled
|
||||||
|
+ 3 // prg banks
|
||||||
|
+ 8 // chr banks
|
||||||
|
+ 1 // mirroring
|
||||||
|
+ 1 // irq_latch
|
||||||
|
+ 1 // irq_counter
|
||||||
|
+ 1 // irq_enabled
|
||||||
|
+ 1 // irq_enabled_after_ack
|
||||||
|
+ 1 // irq_mode_cpu
|
||||||
|
+ 1 // irq_pending
|
||||||
|
+ 2 // irq_prescaler
|
||||||
|
+ 4; // chr blob length
|
||||||
|
if data.len() < min_len {
|
||||||
|
return Err("mapper state is truncated".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
let mut prg_ram = vec![0u8; PRG_RAM_8K];
|
||||||
|
prg_ram.copy_from_slice(&data[cursor..cursor + PRG_RAM_8K]);
|
||||||
|
cursor += PRG_RAM_8K;
|
||||||
|
|
||||||
|
let submapper = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
let prg_ram_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
|
||||||
|
let mut prg_banks_8k = [0u8; 3];
|
||||||
|
prg_banks_8k.copy_from_slice(&data[cursor..cursor + 3]);
|
||||||
|
cursor += 3;
|
||||||
|
|
||||||
|
let mut chr_banks_1k = [0u8; 8];
|
||||||
|
chr_banks_1k.copy_from_slice(&data[cursor..cursor + 8]);
|
||||||
|
cursor += 8;
|
||||||
|
|
||||||
|
let mirroring_default = decode_mirroring(data[cursor]);
|
||||||
|
cursor += 1;
|
||||||
|
let irq_latch = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
let irq_counter = data[cursor];
|
||||||
|
cursor += 1;
|
||||||
|
let irq_enabled = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
let irq_enabled_after_ack = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
let irq_mode_cpu = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
let irq_pending = data[cursor] != 0;
|
||||||
|
cursor += 1;
|
||||||
|
let irq_prescaler = i16::from_le_bytes([data[cursor], data[cursor + 1]]);
|
||||||
|
cursor += 2;
|
||||||
|
|
||||||
|
let mut chr_data = self.chr_data.clone();
|
||||||
|
load_chr_state(&mut chr_data, &data[cursor..])?;
|
||||||
|
|
||||||
|
self.prg_ram = prg_ram;
|
||||||
|
self.submapper = submapper;
|
||||||
|
self.prg_ram_enabled = prg_ram_enabled;
|
||||||
|
self.prg_banks_8k = prg_banks_8k;
|
||||||
|
self.chr_banks_1k = chr_banks_1k;
|
||||||
|
self.mirroring_default = mirroring_default;
|
||||||
|
self.irq_latch = irq_latch;
|
||||||
|
self.irq_counter = irq_counter;
|
||||||
|
self.irq_enabled = irq_enabled;
|
||||||
|
self.irq_enabled_after_ack = irq_enabled_after_ack;
|
||||||
|
self.irq_mode_cpu = irq_mode_cpu;
|
||||||
|
self.irq_pending = irq_pending;
|
||||||
|
self.irq_prescaler = irq_prescaler;
|
||||||
|
self.chr_data = chr_data;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn irq_state(&mut self) -> VrcIrqRegisters<'_> {
|
||||||
|
VrcIrqRegisters {
|
||||||
|
latch: &mut self.irq_latch,
|
||||||
|
counter: &mut self.irq_counter,
|
||||||
|
enabled: &mut self.irq_enabled,
|
||||||
|
enabled_after_ack: &mut self.irq_enabled_after_ack,
|
||||||
|
mode_cpu: &mut self.irq_mode_cpu,
|
||||||
|
pending: &mut self.irq_pending,
|
||||||
|
prescaler: &mut self.irq_prescaler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mapper for Vrc7_85 {
|
||||||
|
fn cpu_read(&self, addr: u16) -> u8 {
|
||||||
|
match addr {
|
||||||
|
0x6000..=0x7FFF => {
|
||||||
|
if self.prg_ram_enabled {
|
||||||
|
self.prg_ram[(addr as usize) - 0x6000]
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x8000..=0xFFFF => {
|
||||||
|
let slot = ((addr - 0x8000) / 0x2000) as usize;
|
||||||
|
let last = self.prg_bank_count_8k().saturating_sub(1);
|
||||||
|
let bank = match slot {
|
||||||
|
0 => self.prg_banks_8k[0] as usize,
|
||||||
|
1 => self.prg_banks_8k[1] as usize,
|
||||||
|
2 => self.prg_banks_8k[2] as usize,
|
||||||
|
_ => last,
|
||||||
|
};
|
||||||
|
read_bank(
|
||||||
|
&self.prg_rom,
|
||||||
|
PRG_BANK_8K,
|
||||||
|
bank,
|
||||||
|
((addr as usize) - 0x8000) & (PRG_BANK_8K - 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
match addr {
|
||||||
|
0x6000..=0x7FFF => {
|
||||||
|
if self.prg_ram_enabled {
|
||||||
|
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x8000..=0xFFFF => {
|
||||||
|
let masked = addr & 0xF018;
|
||||||
|
match masked {
|
||||||
|
0x8000 => self.prg_banks_8k[0] = value & 0x3F,
|
||||||
|
0x9000 => self.prg_banks_8k[2] = value & 0x3F,
|
||||||
|
0x8008 | 0x8010 | 0x8018 => self.prg_banks_8k[1] = value & 0x3F,
|
||||||
|
0xA000 | 0xA008 | 0xA010 | 0xA018 | 0xB000 | 0xB008 | 0xB010 | 0xB018
|
||||||
|
| 0xC000 | 0xC008 | 0xC010 | 0xC018 | 0xD000 | 0xD008 | 0xD010 | 0xD018 => {
|
||||||
|
if let Some(reg) = self.decode_chr_reg(addr) {
|
||||||
|
self.chr_banks_1k[reg] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0xE000 => {
|
||||||
|
self.prg_ram_enabled = (value & 0x80) != 0;
|
||||||
|
self.mirroring_default = match value & 0x03 {
|
||||||
|
0 => Mirroring::Vertical,
|
||||||
|
1 => Mirroring::Horizontal,
|
||||||
|
2 => Mirroring::OneScreenLow,
|
||||||
|
_ => Mirroring::OneScreenHigh,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
0xE008 | 0xE010 | 0xE018 => self.irq_latch = value,
|
||||||
|
0xF000 => vrc_irq_write_control(value, self.irq_state()),
|
||||||
|
0xF008 | 0xF010 | 0xF018 => vrc_irq_ack(self.irq_state()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_read_low(&self, addr: u16) -> Option<u8> {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) && self.prg_ram_enabled {
|
||||||
|
Some(self.prg_ram[(addr as usize) - 0x6000])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_write_low(&mut self, addr: u16, value: u8) -> bool {
|
||||||
|
if (0x6000..=0x7FFF).contains(&addr) {
|
||||||
|
if self.prg_ram_enabled {
|
||||||
|
self.prg_ram[(addr as usize) - 0x6000] = value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_read(&self, addr: u16) -> u8 {
|
||||||
|
if addr > 0x1FFF {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||||
|
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||||
|
read_bank(
|
||||||
|
&self.chr_data,
|
||||||
|
CHR_BANK_1K,
|
||||||
|
bank,
|
||||||
|
(addr as usize) & (CHR_BANK_1K - 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ppu_write(&mut self, addr: u16, value: u8) {
|
||||||
|
if !self.chr_is_ram || addr > 0x1FFF {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let page = (addr / CHR_BANK_1K as u16) as usize;
|
||||||
|
let bank = safe_mod(self.chr_banks_1k[page] as usize, self.chr_bank_count_1k());
|
||||||
|
let idx = bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1));
|
||||||
|
if let Some(cell) = self.chr_data.get_mut(idx) {
|
||||||
|
*cell = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mirroring(&self) -> Mirroring {
|
||||||
|
self.mirroring_default
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clock_cpu(&mut self, cycles: u8) {
|
||||||
|
vrc_irq_clock(cycles, self.irq_state());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_irq(&mut self) -> bool {
|
||||||
|
let out = self.irq_pending;
|
||||||
|
self.irq_pending = false;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_state(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.prg_ram);
|
||||||
|
out.push(self.submapper);
|
||||||
|
out.push(u8::from(self.prg_ram_enabled));
|
||||||
|
out.extend_from_slice(&self.prg_banks_8k);
|
||||||
|
out.extend_from_slice(&self.chr_banks_1k);
|
||||||
|
out.push(encode_mirroring(self.mirroring_default));
|
||||||
|
out.push(self.irq_latch);
|
||||||
|
out.push(self.irq_counter);
|
||||||
|
out.push(u8::from(self.irq_enabled));
|
||||||
|
out.push(u8::from(self.irq_enabled_after_ack));
|
||||||
|
out.push(u8::from(self.irq_mode_cpu));
|
||||||
|
out.push(u8::from(self.irq_pending));
|
||||||
|
out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
|
||||||
|
write_chr_state(out, &self.chr_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_state(&mut self, data: &[u8]) -> Result<(), String> {
|
||||||
|
self.load_native_state(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/native_core/mapper/mod.rs
Normal file
70
src/native_core/mapper/mod.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
mod core;
|
||||||
|
mod mappers;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub use core::Mapper;
|
||||||
|
|
||||||
|
use crate::native_core::ines::InesRom;
|
||||||
|
use mappers::*;
|
||||||
|
|
||||||
|
pub fn create_mapper(rom: InesRom) -> Result<Box<dyn Mapper + Send>, String> {
|
||||||
|
match rom.header.mapper {
|
||||||
|
0 => Ok(Box::new(Nrom::new(rom))),
|
||||||
|
1 => Ok(Box::new(Mmc1::new(rom))),
|
||||||
|
2 => Ok(Box::new(Uxrom::new(rom))),
|
||||||
|
3 => Ok(Box::new(Cnrom::new(rom))),
|
||||||
|
4 => Ok(Box::new(Mmc3::new(rom))),
|
||||||
|
5 => Ok(Box::new(Mmc5::new(rom))),
|
||||||
|
7 => Ok(Box::new(Axrom::new(rom))),
|
||||||
|
9 => Ok(Box::new(Mmc2::new(rom))),
|
||||||
|
10 => Ok(Box::new(Mmc4::new(rom))),
|
||||||
|
11 => Ok(Box::new(ColorDreams11::new(rom))),
|
||||||
|
13 => Ok(Box::new(Cprom13::new(rom))),
|
||||||
|
19 => Ok(Box::new(Namco163_19::new(rom))),
|
||||||
|
21 => Ok(Box::new(Vrc2_23::new(rom))),
|
||||||
|
22 => Ok(Box::new(Vrc2_23::new_with_submapper(rom, 2))),
|
||||||
|
23 => Ok(Box::new(Vrc2_23::new(rom))),
|
||||||
|
24 => Ok(Box::new(Vrc6_24::new(rom, false))),
|
||||||
|
25 => Ok(Box::new(Vrc2_23::new(rom))),
|
||||||
|
26 => Ok(Box::new(Vrc6_24::new(rom, true))),
|
||||||
|
30 => Ok(Box::new(Unrom512_30::new(rom))),
|
||||||
|
66 => Ok(Box::new(Gxrom::new(rom))),
|
||||||
|
69 => Ok(Box::new(Fme7::new(rom))),
|
||||||
|
70 => Ok(Box::new(Bandai70_152::new(rom))),
|
||||||
|
71 => Ok(Box::new(Camerica71::new(rom))),
|
||||||
|
78 => Ok(Box::new(InesMapper78::new(rom))),
|
||||||
|
79 => Ok(Box::new(Nina79::new(rom))),
|
||||||
|
85 => Ok(Box::new(Vrc7_85::new(rom))),
|
||||||
|
87 => Ok(Box::new(InesMapper87::new(rom))),
|
||||||
|
88 => Ok(Box::new(InesMapper88::new(rom))),
|
||||||
|
89 => Ok(Box::new(Bandai70_152::new(rom))),
|
||||||
|
93 => Ok(Box::new(InesMapper93::new(rom))),
|
||||||
|
94 => Ok(Box::new(Un1rom94::new(rom))),
|
||||||
|
95 => Ok(Box::new(InesMapper95::new(rom))),
|
||||||
|
105 => Ok(Box::new(InesMapper105::new(rom))),
|
||||||
|
140 => Ok(Box::new(InesMapper140::new(rom))),
|
||||||
|
155 => Ok(Box::new(InesMapper155::new(rom))),
|
||||||
|
75 => Ok(Box::new(Vrc1_75::new(rom))),
|
||||||
|
118 => Ok(Box::new(InesMapper118::new(rom))), // TxSROM / TLSROM / TKSROM
|
||||||
|
119 => Ok(Box::new(Tqrom119::new(rom))),
|
||||||
|
152 => Ok(Box::new(Bandai70_152::new(rom))),
|
||||||
|
34 => Ok(Box::new(Bnrom34::new(rom))),
|
||||||
|
158 => Ok(Box::new(InesMapper158::new(rom))),
|
||||||
|
64 => Ok(Box::new(InesMapper64::new(rom))),
|
||||||
|
180 => Ok(Box::new(CrazyClimber180::new(rom))),
|
||||||
|
184 => Ok(Box::new(InesMapper184::new(rom))),
|
||||||
|
185 => Ok(Box::new(InesMapper185::new(rom))),
|
||||||
|
206 => {
|
||||||
|
if rom.header.submapper == 1 {
|
||||||
|
Ok(Box::new(InesMapper206Submapper1::new(rom)))
|
||||||
|
} else {
|
||||||
|
Ok(Box::new(InesMapper206::new(rom)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
253 => Ok(Box::new(InesMapper253::new(rom))),
|
||||||
|
mapper => Err(format!("unsupported mapper: {mapper}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
32
src/native_core/mapper/tests.rs
Normal file
32
src/native_core/mapper/tests.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use super::{InesMapper105, Mapper, create_mapper};
|
||||||
|
use crate::native_core::ines::{InesRom, Mirroring};
|
||||||
|
use crate::native_core::test_support::MapperRomBuilder;
|
||||||
|
|
||||||
|
fn test_rom(mapper: u16, prg_banks_16k: u16, chr_banks_8k: u16) -> InesRom {
|
||||||
|
MapperRomBuilder::new(mapper)
|
||||||
|
.prg_banks_16k(prg_banks_16k)
|
||||||
|
.chr_banks_8k(chr_banks_8k)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_rom_with_submapper(
|
||||||
|
mapper: u16,
|
||||||
|
submapper: u8,
|
||||||
|
prg_banks_16k: u16,
|
||||||
|
chr_banks_8k: u16,
|
||||||
|
) -> InesRom {
|
||||||
|
MapperRomBuilder::new(mapper)
|
||||||
|
.submapper(submapper)
|
||||||
|
.prg_banks_16k(prg_banks_16k)
|
||||||
|
.chr_banks_8k(chr_banks_8k)
|
||||||
|
.mirroring(Mirroring::Horizontal)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
mod basic_bank_switch;
|
||||||
|
mod chr_ram_and_conflicts;
|
||||||
|
mod mmc1_105_155_5_19;
|
||||||
|
mod mmc3_vrc_core;
|
||||||
|
mod property_invariants;
|
||||||
|
mod rambo_dxrom_others;
|
||||||
|
mod vrc_variants_mmc2_4;
|
||||||
139
src/native_core/mapper/tests/basic_bank_switch/discrete.rs
Normal file
139
src/native_core/mapper/tests/basic_bank_switch/discrete.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn mapper11_switches_prg_and_chr_banks() {
|
||||||
|
let mut rom = test_rom(11, 8, 8);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // PRG bank 0
|
||||||
|
rom.prg_rom[0x0001] = 0x32; // bus-conflict source for write at $8001
|
||||||
|
rom.prg_rom[0x10000] = 0x20; // PRG bank 2 (2 * 32K)
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x6000] = 0x07; // CHR bank 3 (3 * 8K)
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
mapper.cpu_write(0x8001, 0x32); // PRG=2, CHR=3 with bus-conflict-safe source byte
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper11_applies_bus_conflicts_when_latching_bank_bits() {
|
||||||
|
let mut rom = test_rom(11, 8, 8);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // $8000 currently reads 0x11
|
||||||
|
rom.prg_rom[0x8000] = 0x20; // PRG bank 1
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x2000] = 0x02; // CHR bank 1
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x33); // 0x33 & 0x11 => 0x11
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper71_switches_lower_prg_bank() {
|
||||||
|
let mut rom = test_rom(71, 8, 0);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // bank 0
|
||||||
|
rom.prg_rom[0x8000] = 0x22; // bank 2
|
||||||
|
rom.prg_rom[0x1C000] = 0xFF; // last bank fixed
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||||
|
mapper.cpu_write(0xC000, 0x02);
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x22);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper71_submapper0_ignores_9000_mirroring_control() {
|
||||||
|
let mut rom = test_rom_with_submapper(71, 0, 8, 1);
|
||||||
|
rom.header.mirroring = Mirroring::Vertical;
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||||
|
mapper.cpu_write(0x9000, 0x10);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper71_submapper1_uses_single_screen_mirroring_control() {
|
||||||
|
let rom = test_rom_with_submapper(71, 1, 8, 1);
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||||
|
mapper.cpu_write(0x9000, 0x10);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::OneScreenHigh);
|
||||||
|
mapper.cpu_write(0x9000, 0x00);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper94_uses_shifted_bank_value() {
|
||||||
|
let mut rom = test_rom(94, 8, 0);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // bank 0
|
||||||
|
rom.prg_rom[0xC000] = 0x40; // bank 3
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
mapper.cpu_write(0x8000, 0x0C); // 0x0C >> 2 = 3
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x40);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper180_switches_upper_prg_bank() {
|
||||||
|
let mut rom = test_rom(180, 8, 0);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // fixed first bank
|
||||||
|
rom.prg_rom[0x8000] = 0x33; // bank 2 at upper window
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
mapper.cpu_write(0x8000, 0x02);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0x33);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper34_switches_32k_prg_bank() {
|
||||||
|
let mut rom = test_rom(34, 8, 0);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // 32K bank 0
|
||||||
|
rom.prg_rom[0x10000] = 0x44; // 32K bank 2
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
mapper.cpu_write(0x8000, 0x02);
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x44);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper34_supports_nina001_low_registers() {
|
||||||
|
let mut rom = test_rom(34, 8, 8);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // PRG bank 0 (32K)
|
||||||
|
rom.prg_rom[0x8000] = 0x20; // PRG bank 1 (32K)
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR 4K bank 0
|
||||||
|
rom.chr_data[0x1000] = 0x02; // CHR 4K bank 1
|
||||||
|
rom.chr_data[0x3000] = 0x04; // CHR 4K bank 3
|
||||||
|
rom.chr_data[0x4000] = 0x05; // CHR 4K bank 4
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
assert_eq!(mapper.ppu_read(0x1000), 0x02);
|
||||||
|
|
||||||
|
assert!(mapper.cpu_write_low(0x7FFD, 0x01)); // PRG bank
|
||||||
|
assert!(mapper.cpu_write_low(0x7FFE, 0x03)); // CHR low 4K
|
||||||
|
assert!(mapper.cpu_write_low(0x7FFF, 0x04)); // CHR high 4K
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x04);
|
||||||
|
assert_eq!(mapper.ppu_read(0x1000), 0x05);
|
||||||
|
}
|
||||||
46
src/native_core/mapper/tests/basic_bank_switch/mapper78.rs
Normal file
46
src/native_core/mapper/tests/basic_bank_switch/mapper78.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn mapper78_switches_prg_chr_and_mirroring() {
|
||||||
|
let mut rom = test_rom(78, 8, 8);
|
||||||
|
rom.prg_rom.fill(0xFF);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // PRG bank 0 lower window
|
||||||
|
rom.prg_rom[0x8000] = 0x20; // PRG bank 2 lower window
|
||||||
|
rom.prg_rom[0x1C000] = 0xFE; // fixed last bank
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x6000] = 0x07; // CHR bank 3
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0xFE);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
mapper.cpu_write(0x8001, 0x3A); // avoid bus conflict masking at $8000 byte
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::OneScreenHigh);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper78_submapper3_uses_horizontal_vertical_mirroring() {
|
||||||
|
let mut rom = test_rom_with_submapper(78, 3, 8, 2);
|
||||||
|
rom.prg_rom.fill(0xFF);
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x00);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||||
|
mapper.cpu_write(0x8000, 0x08);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::Vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper78_applies_bus_conflicts() {
|
||||||
|
let mut rom = test_rom_with_submapper(78, 1, 8, 2);
|
||||||
|
rom.prg_rom.fill(0xFF);
|
||||||
|
// At $8000 lower PRG bank maps to byte 0x07, so write $08 becomes $00.
|
||||||
|
rom.prg_rom[0x0000] = 0x07;
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x08);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x07);
|
||||||
|
}
|
||||||
5
src/native_core/mapper/tests/basic_bank_switch/mod.rs
Normal file
5
src/native_core/mapper/tests/basic_bank_switch/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod discrete;
|
||||||
|
mod mapper78;
|
||||||
|
mod smoke_and_fme7;
|
||||||
138
src/native_core/mapper/tests/basic_bank_switch/smoke_and_fme7.rs
Normal file
138
src/native_core/mapper/tests/basic_bank_switch/smoke_and_fme7.rs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn supports_known_mapper_ids() {
|
||||||
|
for mapper in [
|
||||||
|
0u16, 1, 2, 3, 4, 5, 7, 9, 10, 11, 13, 19, 21, 22, 23, 24, 25, 26, 30, 34, 64, 66, 69, 70,
|
||||||
|
71, 75, 78, 79, 85, 87, 88, 89, 93, 94, 95, 105, 118, 119, 140, 152, 155, 158, 180, 184,
|
||||||
|
185, 206, 253,
|
||||||
|
] {
|
||||||
|
let rom = test_rom(mapper, 8, 4);
|
||||||
|
let m = create_mapper(rom).expect("mapper should be created");
|
||||||
|
let _ = m.cpu_read(0x8000);
|
||||||
|
let _ = m.ppu_read(0x0000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nrom_state_roundtrip() {
|
||||||
|
let rom = test_rom(0, 1, 0);
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
mapper.ppu_write(0x0010, 0xAB);
|
||||||
|
|
||||||
|
let mut saved = Vec::new();
|
||||||
|
mapper.save_state(&mut saved);
|
||||||
|
|
||||||
|
let rom2 = test_rom(0, 1, 0);
|
||||||
|
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||||
|
restored.load_state(&saved).expect("state must load");
|
||||||
|
assert_eq!(restored.ppu_read(0x0010), 0xAB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nrom_16k_prg_mirrors_into_upper_half() {
|
||||||
|
let mut rom = test_rom(0, 1, 1);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0xA1;
|
||||||
|
rom.prg_rom[0x3FFF] = 0xB2;
|
||||||
|
let mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0xA1);
|
||||||
|
assert_eq!(mapper.cpu_read(0xBFFF), 0xB2);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0xA1);
|
||||||
|
assert_eq!(mapper.cpu_read(0xFFFF), 0xB2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nrom_low_window_is_not_backend_prg_ram() {
|
||||||
|
let rom = test_rom(0, 2, 1);
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read_low(0x6000), None);
|
||||||
|
assert!(!mapper.cpu_write_low(0x6000, 0xAB));
|
||||||
|
assert_eq!(mapper.cpu_read_low(0x6000), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uxrom_switches_bank() {
|
||||||
|
let mut rom = test_rom(2, 4, 1);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10;
|
||||||
|
rom.prg_rom[0x4000] = 0x20;
|
||||||
|
rom.prg_rom[0x8000] = 0x30;
|
||||||
|
rom.prg_rom[0xC000] = 0x40;
|
||||||
|
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0x40);
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 2);
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x30);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unknown_mapper() {
|
||||||
|
let rom = test_rom(255, 1, 1);
|
||||||
|
assert!(create_mapper(rom).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fme7_accepts_command_and_data_across_port_ranges() {
|
||||||
|
let mut rom = test_rom(69, 8, 1);
|
||||||
|
for bank in 0..8usize {
|
||||||
|
rom.chr_data[bank * 0x400] = bank as u8;
|
||||||
|
}
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x9FFF, 0x00);
|
||||||
|
mapper.cpu_write(0xB123, 0x03);
|
||||||
|
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fme7_chr_ram_write_uses_selected_bank() {
|
||||||
|
let rom = test_rom(69, 8, 0);
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x00);
|
||||||
|
mapper.cpu_write(0xA000, 0x01);
|
||||||
|
mapper.ppu_write(0x0012, 0xAA);
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x00);
|
||||||
|
mapper.cpu_write(0xA000, 0x00);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0012), 0x00);
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x00);
|
||||||
|
mapper.cpu_write(0xA000, 0x01);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0012), 0xAA);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fme7_low_window_defaults_to_prg_rom_when_ram_disabled() {
|
||||||
|
let mut rom = test_rom(69, 8, 1);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // 8K bank 0 @ $6000 when RAM disabled
|
||||||
|
rom.prg_rom[0x2000] = 0x22; // 8K bank 1
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x11));
|
||||||
|
mapper.cpu_write(0x8000, 0x08);
|
||||||
|
mapper.cpu_write(0xA000, 0x01); // RAM disabled, select ROM bank 1
|
||||||
|
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x22));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fme7_low_window_uses_ram_only_when_enabled() {
|
||||||
|
let mut rom = test_rom(69, 8, 1);
|
||||||
|
rom.prg_rom.fill(0x33);
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x08);
|
||||||
|
mapper.cpu_write(0xA000, 0xC0); // Select RAM + enable RAM in $6000-$7FFF
|
||||||
|
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x00));
|
||||||
|
assert!(mapper.cpu_write_low(0x6123, 0xA5));
|
||||||
|
assert_eq!(mapper.cpu_read_low(0x6123), Some(0xA5));
|
||||||
|
|
||||||
|
mapper.cpu_write(0xA000, 0x00); // Disable RAM -> back to ROM view
|
||||||
|
assert_eq!(mapper.cpu_read_low(0x6000), Some(0x33));
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper3_allows_chr_ram_write_when_chr_is_ram() {
|
||||||
|
let mut rom = test_rom(3, 2, 0);
|
||||||
|
rom.chr_data = vec![0; 0x4000];
|
||||||
|
rom.chr_is_ram = true;
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x01);
|
||||||
|
mapper.ppu_write(0x0012, 0xA5);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0012), 0xA5);
|
||||||
|
mapper.cpu_write(0x8000, 0x00);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0012), 0x00);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper3_submapper2_applies_and_bus_conflicts() {
|
||||||
|
let mut rom = test_rom_with_submapper(3, 2, 2, 4);
|
||||||
|
rom.prg_rom.fill(0xFF);
|
||||||
|
rom.prg_rom[0x0000] = 0x01; // $8000 bus value used for conflict mask
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.chr_data[0x0000] = 0x10; // CHR bank 0 marker
|
||||||
|
rom.chr_data[0x2000] = 0x11; // CHR bank 1 marker
|
||||||
|
rom.chr_data[0x4000] = 0x12; // CHR bank 2 marker
|
||||||
|
rom.chr_data[0x6000] = 0x13; // CHR bank 3 marker
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x03); // bus conflict -> 0x03 & 0x01 = 0x01
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x11);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper7_submapper2_applies_and_bus_conflicts() {
|
||||||
|
let mut rom = test_rom_with_submapper(7, 2, 8, 1);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x02; // CPU sees this byte at $8000 during write
|
||||||
|
rom.prg_rom[0x10000] = 0xA2; // 32K bank 2 marker at $8000
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x01); // with conflicts latched=0x00 -> bank 0
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x02);
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x13); // with conflicts latched=0x02 -> bank 2, one-screen low
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0xA2);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::OneScreenLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper7_submapper1_has_no_bus_conflicts() {
|
||||||
|
let mut rom = test_rom_with_submapper(7, 1, 8, 1);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x02; // if conflicts existed, bit0 would be masked off
|
||||||
|
rom.prg_rom[0x8000] = 0xA1; // 32K bank 1 marker at $8000
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x01); // no conflicts -> bank 1 selected
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0xA1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper3_chr_ram_state_roundtrip_uses_strict_payload() {
|
||||||
|
let mut rom = test_rom(3, 2, 0);
|
||||||
|
rom.chr_data = vec![0; 0x4000];
|
||||||
|
rom.chr_is_ram = true;
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x01);
|
||||||
|
mapper.ppu_write(0x0020, 0xA6);
|
||||||
|
|
||||||
|
let mut state = Vec::new();
|
||||||
|
mapper.save_state(&mut state);
|
||||||
|
assert!(
|
||||||
|
state.len() > 8,
|
||||||
|
"mapper 3 state must include CHR payload blob"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rom2 = test_rom(3, 2, 0);
|
||||||
|
rom2.chr_data = vec![0; 0x4000];
|
||||||
|
rom2.chr_is_ram = true;
|
||||||
|
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||||
|
restored.load_state(&state).expect("state load");
|
||||||
|
assert_eq!(restored.ppu_read(0x0020), 0xA6);
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper75_switches_prg_chr_and_mirroring() {
|
||||||
|
let mut rom = test_rom(75, 16, 8);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // PRG bank 0
|
||||||
|
rom.prg_rom[0x2000] = 0x11; // PRG bank 1
|
||||||
|
rom.prg_rom[0x6000] = 0x13; // PRG bank 3
|
||||||
|
rom.prg_rom[0x3E000] = 0x1F; // fixed last bank
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x2000] = 0x03; // CHR bank 2
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
mapper.cpu_write(0x8000, 0x01);
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
mapper.cpu_write(0xC000, 0x03);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0x13);
|
||||||
|
assert_eq!(mapper.cpu_read(0xE000), 0x1F);
|
||||||
|
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
mapper.cpu_write(0xE000, 0x02);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||||
|
|
||||||
|
mapper.cpu_write(0x9000, 0x01);
|
||||||
|
assert_eq!(mapper.mirroring(), Mirroring::Horizontal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper140_selects_prg_chr_from_low_window_write() {
|
||||||
|
let mut rom = test_rom(140, 8, 16);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // 32K bank 0
|
||||||
|
rom.prg_rom[0x8000] = 0x20; // 32K bank 1
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x6000] = 0x04; // CHR bank 3
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
assert!(mapper.cpu_write_low(0x6000, 0x13)); // PRG=1 CHR=3
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x20);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x04);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper184_selects_two_4k_chr_banks_from_low_window_write() {
|
||||||
|
let mut rom = test_rom(184, 2, 16);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.chr_data[0x0000] = 0x11; // bank 0
|
||||||
|
rom.chr_data[0x5000] = 0x55; // bank 5
|
||||||
|
rom.chr_data[0x6000] = 0x66; // bank 6
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert!(mapper.cpu_write_low(0x6000, 0x65)); // low=5, high=(6|4)=10
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x55);
|
||||||
|
assert_eq!(mapper.ppu_read(0x1000), 0x66);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper79_switches_prg_and_chr_from_low_window_write() {
|
||||||
|
let mut rom = test_rom(79, 4, 8);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // PRG 32K bank 0
|
||||||
|
rom.prg_rom[0x8000] = 0x22; // PRG 32K bank 1
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x6000] = 0x07; // CHR bank 3
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
assert!(mapper.cpu_write_low(0x4100, 0x0B)); // PRG=1 CHR=3
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x22);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper79_only_accepts_masked_low_register_addresses() {
|
||||||
|
let mut rom = test_rom(79, 4, 8);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // PRG 32K bank 0
|
||||||
|
rom.prg_rom[0x8000] = 0x22; // PRG 32K bank 1
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x6000] = 0x07; // CHR bank 3
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert!(!mapper.cpu_write_low(0x4200, 0x0B)); // A8=0 -> not a register
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
|
||||||
|
assert!(!mapper.cpu_write_low(0x6100, 0x0B)); // outside $4xxx/$5xxx
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
|
||||||
|
assert!(mapper.cpu_write_low(0x5F00, 0x0B)); // valid masked register
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x22);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x07);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper79_chr_ram_write_and_state_roundtrip() {
|
||||||
|
let mut rom = test_rom(79, 4, 0);
|
||||||
|
rom.chr_data = vec![0; 0x8000];
|
||||||
|
rom.chr_is_ram = true;
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert!(mapper.cpu_write_low(0x4100, 0x09)); // PRG=1 CHR=1
|
||||||
|
mapper.ppu_write(0x0010, 0xA5);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0010), 0xA5);
|
||||||
|
|
||||||
|
let mut state = Vec::new();
|
||||||
|
mapper.save_state(&mut state);
|
||||||
|
let mut rom2 = test_rom(79, 4, 0);
|
||||||
|
rom2.chr_data = vec![0; 0x8000];
|
||||||
|
rom2.chr_is_ram = true;
|
||||||
|
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||||
|
restored.load_state(&state).expect("state load");
|
||||||
|
assert!(restored.cpu_write_low(0x4100, 0x09));
|
||||||
|
assert_eq!(restored.ppu_read(0x0010), 0xA5);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn mapper78_chr_ram_state_roundtrip() {
|
||||||
|
let mut rom = test_rom_with_submapper(78, 1, 4, 0);
|
||||||
|
rom.prg_rom.fill(0xFF);
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x10); // CHR bank 1
|
||||||
|
mapper.ppu_write(0x0123, 0x5A);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0123), 0x5A);
|
||||||
|
|
||||||
|
let mut state = Vec::new();
|
||||||
|
mapper.save_state(&mut state);
|
||||||
|
|
||||||
|
let mut rom2 = test_rom_with_submapper(78, 1, 4, 0);
|
||||||
|
rom2.prg_rom.fill(0xFF);
|
||||||
|
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||||
|
restored.load_state(&state).expect("state must load");
|
||||||
|
assert_eq!(restored.ppu_read(0x0123), 0x5A);
|
||||||
|
assert_eq!(restored.mirroring(), Mirroring::OneScreenLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper87_switches_chr_bank() {
|
||||||
|
let mut rom = test_rom(87, 2, 4);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x2000] = 0x02; // CHR bank 1
|
||||||
|
rom.chr_data[0x4000] = 0x03; // CHR bank 2
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
assert!(mapper.cpu_write_low(0x6000, 0x01)); // LH -> bank 2
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||||
|
assert!(mapper.cpu_write_low(0x6000, 0x02)); // LH -> bank 1
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper87_chr_ram_write_and_state_roundtrip() {
|
||||||
|
let mut rom = test_rom(87, 2, 0);
|
||||||
|
rom.chr_data = vec![0; 0x4000];
|
||||||
|
rom.chr_is_ram = true;
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert!(mapper.cpu_write_low(0x6000, 0x02));
|
||||||
|
mapper.ppu_write(0x0010, 0x7C);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0010), 0x7C);
|
||||||
|
|
||||||
|
let mut state = Vec::new();
|
||||||
|
mapper.save_state(&mut state);
|
||||||
|
let mut rom2 = test_rom(87, 2, 0);
|
||||||
|
rom2.chr_data = vec![0; 0x4000];
|
||||||
|
rom2.chr_is_ram = true;
|
||||||
|
let mut restored = create_mapper(rom2).expect("must create mapper");
|
||||||
|
restored.load_state(&state).expect("state load");
|
||||||
|
assert!(restored.cpu_write_low(0x6000, 0x02));
|
||||||
|
assert_eq!(restored.ppu_read(0x0010), 0x7C);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper87_ignores_cpu_write_high_window() {
|
||||||
|
let mut rom = test_rom(87, 2, 4);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x2000] = 0x02; // CHR bank 1
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.cpu_write(0x8000, 0x01);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
assert!(mapper.cpu_write_low(0x7000, 0x02));
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x02);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper93_switches_lower_prg_bank_using_high_nibble() {
|
||||||
|
let mut rom = test_rom(93, 8, 0);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // bank 0
|
||||||
|
rom.prg_rom[0x8000] = 0x30; // bank 2
|
||||||
|
rom.prg_rom[0x1C000] = 0xFF; // fixed last bank
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||||
|
mapper.cpu_write(0x8000, 0x20); // 0x2 in high nibble
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x30);
|
||||||
|
assert_eq!(mapper.cpu_read(0xC000), 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper30_switches_prg_and_chr_ram_bank() {
|
||||||
|
let mut rom = test_rom(30, 8, 0);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x11; // PRG bank 0
|
||||||
|
rom.prg_rom[0x8000] = 0x33; // PRG bank 2
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x11);
|
||||||
|
mapper.cpu_write(0x8000, 0x22); // PRG=2, CHR=1
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x33);
|
||||||
|
mapper.ppu_write(0x0010, 0xA5);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0010), 0xA5);
|
||||||
|
mapper.cpu_write(0x8000, 0x02); // CHR bank 0
|
||||||
|
assert_eq!(mapper.ppu_read(0x0010), 0x00);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper70_switches_prg_and_chr() {
|
||||||
|
let mut rom = test_rom(70, 8, 8);
|
||||||
|
rom.prg_rom.fill(0);
|
||||||
|
rom.chr_data.fill(0);
|
||||||
|
rom.prg_rom[0x0000] = 0x10; // PRG bank 0
|
||||||
|
rom.prg_rom[0x8000] = 0x30; // PRG bank 2
|
||||||
|
rom.chr_data[0x0000] = 0x01; // CHR bank 0
|
||||||
|
rom.chr_data[0x4000] = 0x03; // CHR bank 2
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x10);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x01);
|
||||||
|
mapper.cpu_write(0x8000, 0x22); // CHR=2 PRG=2
|
||||||
|
assert_eq!(mapper.cpu_read(0x8000), 0x30);
|
||||||
|
assert_eq!(mapper.ppu_read(0x0000), 0x03);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapper13_switches_upper_4k_chr_ram_bank() {
|
||||||
|
let mut rom = test_rom(13, 2, 0);
|
||||||
|
rom.chr_data = vec![0; 0x4000];
|
||||||
|
rom.chr_is_ram = true;
|
||||||
|
let mut mapper = create_mapper(rom).expect("must create mapper");
|
||||||
|
|
||||||
|
mapper.ppu_write(0x1000, 0xA1); // bank 0
|
||||||
|
assert_eq!(mapper.ppu_read(0x1000), 0xA1);
|
||||||
|
mapper.cpu_write(0x8000, 0x01);
|
||||||
|
assert_eq!(mapper.ppu_read(0x1000), 0x00);
|
||||||
|
mapper.ppu_write(0x1000, 0xB2); // bank 1
|
||||||
|
assert_eq!(mapper.ppu_read(0x1000), 0xB2);
|
||||||
|
mapper.cpu_write(0x8000, 0x00);
|
||||||
|
assert_eq!(mapper.ppu_read(0x1000), 0xA1);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user