Compare commits

...

20 Commits

Author SHA1 Message Date
Logan McNaughton ff093a9f7f Update README.md 2025-02-27 15:55:07 +01:00
Logan McNaughton f7cb0db348 update cargo packages 2025-02-27 11:31:18 +01:00
Logan McNaughton eb9f37f1f5 add button to open save folder (#289) 2025-02-27 11:29:30 +01:00
Logan McNaughton 37b902eb31 use deterministic rtc time (#288)
* use deterministic rtc time

* netplay
2025-02-27 10:59:44 +01:00
Logan McNaughton 00964f06f0 use GL backend for GUI to avoid conflicts with game window (#287) 2025-02-27 10:04:53 +01:00
Logan McNaughton 319d7c8553 some mbc3 rtc code (#284) 2025-02-26 21:00:15 +01:00
Logan McNaughton 82b4adb4ad flash cart menu support (#283)
* flash cart menu support

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-02-26 10:04:44 +01:00
Logan McNaughton 8fbc63a677 add links to wiki and discord (#282) 2025-02-24 18:50:29 +01:00
Logan McNaughton 48ee50dea3 fix Stadium 2 GB games (#281) 2025-02-24 15:31:19 +01:00
Logan McNaughton 34a09925b4 fix GB tower (#280) 2025-02-24 14:43:06 +01:00
Logan McNaughton 168347b46e Transfer Pak UI (#279)
* Transfer Pak UI

* more

* more

* more

* more
2025-02-24 12:49:33 +01:00
Logan McNaughton 95a3d3d5ec fix type 2025-02-23 22:28:07 +01:00
Logan McNaughton f6349f13da tpak cleanup (#278)
* tpak cleanup

* more

* more
2025-02-23 16:10:53 +01:00
Logan McNaughton fb2cf7fcb9 move UI elements around (#277) 2025-02-23 13:25:23 +01:00
Logan McNaughton 9980302d37 more tpak code (#276)
* more tpak code

* more
2025-02-23 13:10:50 +01:00
Logan McNaughton 4692e2bcb9 more tpak code (#275)
* more tpak code

* more
2025-02-23 11:59:15 +01:00
Logan McNaughton 04390301c9 more tpak code (#274) 2025-02-22 21:41:21 +01:00
Logan McNaughton bc415821d6 check for new version (#273)
* check for new version

* more
2025-02-21 23:02:16 +01:00
Logan McNaughton a068363787 Add note about command line flatpak usage 2025-02-20 20:12:25 +01:00
Logan McNaughton 97839fae40 Bump to 1.0.9 (#272) 2025-02-20 18:44:15 +01:00
23 changed files with 948 additions and 212 deletions
Generated
+68 -58
View File
@@ -438,12 +438,11 @@ checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]]
name = "bzip2"
version = "0.4.4"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
checksum = "75b89e7c29231c673a61a46e722602bcd138298f6b9e81e71119693534585f5c"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
@@ -485,9 +484,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.14"
version = "1.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
dependencies = [
"jobserver",
"libc",
@@ -532,16 +531,16 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.39"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
"windows-link",
]
[[package]]
@@ -567,9 +566,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.30"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
dependencies = [
"clap_builder",
"clap_derive",
@@ -577,9 +576,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.30"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
dependencies = [
"anstream",
"anstyle",
@@ -1027,9 +1026,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.13.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
[[package]]
name = "emath"
@@ -1200,9 +1199,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.0.35"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1374,8 +1373,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"wasm-bindgen",
"windows-targets 0.52.6",
]
@@ -1482,7 +1483,7 @@ dependencies = [
[[package]]
name = "gopher64"
version = "1.0.8"
version = "1.0.9"
dependencies = [
"bindgen",
"cc",
@@ -1500,6 +1501,7 @@ dependencies = [
"rfd",
"rusttype",
"sdl3-sys",
"semver",
"serde",
"serde-big-array",
"serde_json",
@@ -1515,23 +1517,25 @@ dependencies = [
[[package]]
name = "governor"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "842dc78579ce01e6a1576ad896edc92fca002dd60c9c3746b7fc2bec6fb429d0"
checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb"
dependencies = [
"cfg-if",
"dashmap",
"futures-sink",
"futures-timer",
"futures-util",
"getrandom 0.3.1",
"no-std-compat",
"nonzero_ext",
"parking_lot",
"portable-atomic",
"quanta",
"rand 0.8.5",
"rand 0.9.0",
"smallvec",
"spinning_top",
"web-time",
]
[[package]]
@@ -1956,9 +1960,9 @@ dependencies = [
[[package]]
name = "inout"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
@@ -2056,9 +2060,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]]
name = "libloading"
@@ -2088,7 +2092,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.8.0",
"libc",
"redox_syscall 0.5.8",
"redox_syscall 0.5.9",
]
[[package]]
@@ -2099,9 +2103,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "litemap"
version = "0.7.4"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]]
name = "litrs"
@@ -2127,9 +2131,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.25"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "lzma-rs"
@@ -2221,9 +2225,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [
"adler2",
"simd-adler32",
@@ -2691,7 +2695,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.8",
"redox_syscall 0.5.9",
"smallvec",
"windows-targets 0.52.6",
]
@@ -2792,9 +2796,9 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "portable-atomic"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "postcard"
@@ -2961,7 +2965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.1",
"rand_core 0.9.2",
"zerocopy 0.8.20",
]
@@ -2982,7 +2986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.1",
"rand_core 0.9.2",
]
[[package]]
@@ -2996,9 +3000,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3"
checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.20",
@@ -3030,9 +3034,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [
"bitflags 2.8.0",
]
@@ -3154,9 +3158,9 @@ dependencies = [
[[package]]
name = "ring"
version = "0.17.9"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24"
checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
dependencies = [
"cc",
"cfg-if",
@@ -4336,9 +4340,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "24.0.0"
version = "24.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a39b8842dc9ffcbe34346e3ab6d496b32a47f6497e119d762c97fcaae3cb37"
checksum = "671c25545d479b47d3f0a8e373aceb2060b67c6eb841b24ac8c32348151c7a0c"
dependencies = [
"arrayvec",
"bit-vec 0.8.0",
@@ -4361,9 +4365,9 @@ dependencies = [
[[package]]
name = "wgpu-hal"
version = "24.0.0"
version = "24.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a782e5056b060b0b4010881d1decddd059e44f2ecd01e2db2971b48ad3627e5"
checksum = "4317a17171dc20e6577bf606796794580accae0716a69edbc7388c86a3ec9f23"
dependencies = [
"android_system_properties",
"arrayvec",
@@ -4498,6 +4502,12 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -5029,18 +5039,18 @@ dependencies = [
[[package]]
name = "zerofrom"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
@@ -5092,9 +5102,9 @@ dependencies = [
[[package]]
name = "zip"
version = "2.2.2"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45"
checksum = "b280484c454e74e5fff658bbf7df8fdbe7a07c6b2de4a53def232c15ef138f3a"
dependencies = [
"aes",
"arbitrary",
@@ -5135,27 +5145,27 @@ dependencies = [
[[package]]
name = "zstd"
version = "0.13.2"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.1"
version = "7.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.13+zstd.1.5.6"
version = "2.0.14+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5"
dependencies = [
"cc",
"pkg-config",
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "gopher64"
version = "1.0.8"
version = "1.0.9"
edition = "2024"
rust-version = "1.85"
@@ -28,6 +28,7 @@ regex = "1.11"
tungstenite = "0.26"
socket2 = "0.5"
fatfs = "0.3"
semver = "1.0"
postcard = { version = "1.1", features = ["use-std"] }
clap = { version = "4.5", features = ["derive"] }
reqwest = { version = "0.12", default-features = false, features = ["http2","rustls-tls","charset","json"] }
+11 -5
View File
@@ -1,5 +1,7 @@
# gopher64
Gopher64 can be used via its GUI or the command line.
## download
https://github.com/gopher64/gopher64/releases
## wiki
@@ -25,6 +27,14 @@ Gopher64 supports netplay (online play with others). It has a few public netplay
If you would like to keep all the game data in the same folder as executable, you just need to create a file called "portable.txt" in the same directory as the executable.
## flatpak
If you want to run the flatpak from the command line, you need to add the `--filesystem=host:ro` option, for example:
```
flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
```
## goals
1. Performance. I want to be able to use this emulator on my laptop.
@@ -47,7 +57,3 @@ I am very open to contributions! Please reach out to me via a GitHub issue, or v
## license
Gopher64 is licensed under the GPLv3 license. Many portions of gopher64 have been adapted from mupen64plus and/or ares. The license for mupen64plus can be found here: https://github.com/mupen64plus/mupen64plus-core/blob/master/LICENSES. The license for ares can be found here: https://github.com/ares-emulator/ares/blob/master/LICENSE.
## where to download
https://github.com/gopher64/gopher64/releases
+12 -5
View File
@@ -72,7 +72,7 @@ pub fn run_game(rom_contents: Vec<u8>, device: &mut Device) {
ui::input::close(&mut device.ui);
ui::video::close(&device.ui);
ui::audio::close(&mut device.ui);
ui::storage::write_saves(&device.ui, &device.netplay);
ui::storage::write_saves(device);
}
fn swap_rom(contents: Vec<u8>) -> Vec<u8> {
@@ -176,6 +176,7 @@ pub struct Device {
pub ri: ri::Ri,
pub vru: controller::vru::Vru,
pub vru_window: controller::vru::VruWindow,
pub transferpaks: [controller::transferpak::TransferPak; 4],
}
pub fn zero_m128i() -> __m128i {
@@ -297,19 +298,19 @@ impl Device {
},
cart: cart::Cart {
rom: Vec::new(),
is_viewer_buffer: [0; 0xFFFF],
is_viewer_buffer: vec![0; 0xFFFF],
pal: false,
latch: 0,
cic_seed: 0,
cic_type: cart::CicType::CicNus6102,
rdram_size_offset: 0,
rtc_timestamp: 0,
rtc: cart::AfRtc { control: 0x0200 },
sc64: cart::sc64::Sc64 {
regs: [0; cart::sc64::SC64_REGS_COUNT as usize],
regs_locked: true,
cfg: [0; cart::sc64::SC64_CFG_COUNT as usize],
sector: 0,
buffer: [0; 8192],
buffer: vec![0; 8192],
writeback_sector: vec![0; 256],
},
flashram: cart::sram::Flashram {
status: 0,
@@ -460,6 +461,12 @@ impl Device {
talking: false,
word_mappings: HashMap::new(),
},
transferpaks: [
controller::transferpak::TransferPak::default(),
controller::transferpak::TransferPak::default(),
controller::transferpak::TransferPak::default(),
controller::transferpak::TransferPak::default(),
],
}
}
}
+6 -6
View File
@@ -45,16 +45,14 @@ fn byte2bcd(mut n: u32) -> u8 {
pub struct Cart {
#[serde(skip)]
pub rom: Vec<u8>,
#[serde(with = "serde_big_array::BigArray")]
pub is_viewer_buffer: [u8; 0xFFFF],
pub is_viewer_buffer: Vec<u8>,
pub pal: bool,
pub latch: u32,
pub cic_type: CicType,
pub cic_seed: u8,
pub rdram_size_offset: usize,
pub rtc: AfRtc,
pub sc64: sc64::Sc64,
pub flashram: sram::Flashram,
pub rtc_timestamp: i64,
}
pub fn process(device: &mut device::Device, channel: usize) {
@@ -125,7 +123,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
}
}
fn format_eeprom(device: &mut device::Device) {
pub fn format_eeprom(device: &mut device::Device) {
if device.ui.saves.eeprom.data.len() < EEPROM_MAX_SIZE {
device.ui.saves.eeprom.data.resize(EEPROM_MAX_SIZE, 0xFF)
}
@@ -154,7 +152,9 @@ fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize,
}
fn time2data(device: &mut device::Device, offset: usize) {
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
let timestamp =
device.cart.rtc_timestamp + (device.vi.frame_time * device.vi.vi_counter as f64) as i64;
let now = chrono::DateTime::from_timestamp(timestamp, 0).unwrap();
device.pif.ram[offset] = byte2bcd(now.second());
device.pif.ram[offset + 1] = byte2bcd(now.minute());
+42 -47
View File
@@ -1,19 +1,16 @@
use crate::device;
use crate::netplay;
use sha2::{Digest, Sha256};
const CART_MASK: usize = 0xFFFFFFF;
fn read_cart_word(device: &mut device::Device, address: usize) -> u32 {
let mut data: [u8; 4] = device.cart.rom[address..address + 4].try_into().unwrap();
let mut data: [u8; 4] = [0; 4];
for i in 0..4 {
if device
.ui
.saves
.romsave
.data
.contains_key(&(address as u32 + i))
{
data[i as usize] = device.ui.saves.romsave.data[&(address as u32 + i)];
if let Some(value) = device.ui.saves.romsave.data.get(&(address as u32 + i)) {
data[i as usize] = *value;
} else {
data[i as usize] = *device.cart.rom.get(address + i as usize).unwrap_or(&0);
}
}
u32::from_be_bytes(data)
@@ -46,7 +43,9 @@ pub fn read_mem(
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0 {
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0
&& device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] == 0
{
let masked_address = address as usize & CART_MASK;
let mut data = read_cart_word(device, masked_address);
device::memory::masked_write_32(&mut data, value, mask);
@@ -79,20 +78,19 @@ pub fn dma_read(
mut dram_addr: u32,
length: u32,
) -> u64 {
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0 {
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0
&& device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] == 0
{
dram_addr &= device::rdram::RDRAM_MASK as u32;
cart_addr &= CART_MASK as u32;
for i in 0..length {
if cart_addr + i < device.cart.rom.len() as u32 {
device.ui.saves.romsave.data.insert(
cart_addr + i,
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap],
);
device.ui.saves.romsave.written = true;
}
device.ui.saves.romsave.data.insert(
cart_addr + i,
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap],
);
}
device.ui.saves.romsave.written = true;
}
device::pi::calculate_cycles(device, 1, length)
@@ -109,24 +107,34 @@ pub fn dma_write(
cart_addr &= CART_MASK as u32;
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && j < device.cart.rom.len() as u32 {
while i < dram_addr + length {
if device.ui.saves.romsave.data.contains_key(&j) {
device.rdram.mem[i as usize ^ device.byte_swap] = device.ui.saves.romsave.data[&j];
} else {
device.rdram.mem[i as usize ^ device.byte_swap] = device.cart.rom[j as usize];
device.rdram.mem[i as usize ^ device.byte_swap] =
*device.cart.rom.get(j as usize).unwrap_or(&0);
}
i += 1;
j += 1;
}
while i < dram_addr + length {
// DMAs that extend past the end of the ROM return 0's for the portion that extends past the ROM length
device.rdram.mem[i as usize ^ device.byte_swap] = 0;
i += 1;
}
device::pi::calculate_cycles(device, 1, length)
}
pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
if device.netplay.is_some() {
let netplay = device.netplay.as_mut().unwrap();
if netplay.player_number == 0 {
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
} else {
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
}
}
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
device.cart.rom = rom_file;
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
set_cic(device);
@@ -153,40 +161,27 @@ fn set_cic(device: &mut device::Device) {
let hash = calculate_hash(&device.cart.rom[0x40..0x1000]);
match hash.as_str() {
"B99F06C4802C2377E31E388435955EF3E99C618A6D55D24699D828EB1075F1EB" => {
device.cart.cic_type = device::cart::CicType::CicNus6101;
device.cart.cic_seed = 0x3F;
device.cart.rdram_size_offset = 0x318;
device.cart.cic_seed = 0x3F; // CicNus6101
}
"61E88238552C356C23D19409FE5570EE6910419586BC6FC740F638F761ADC46E" => {
device.cart.cic_type = device::cart::CicType::CicNus6102;
device.cart.cic_seed = 0x3F;
device.cart.rdram_size_offset = 0x318;
device.cart.cic_seed = 0x3F; // CicNus6102
}
"BF3620D30817007091EBE9BDDD1B88C23B8A0052170B3309CDE5B6B4238E45E7" => {
device.cart.cic_type = device::cart::CicType::CicNus6103;
device.cart.cic_seed = 0x78;
device.cart.rdram_size_offset = 0x318;
device.cart.cic_seed = 0x78; // CicNus6103
}
"04B7BC6717A9F0EB724CF927E74AD3876C381CBB280D841736FC5E55580B756B" => {
device.cart.cic_type = device::cart::CicType::CicNus6105;
device.cart.cic_seed = 0x91;
device.cart.rdram_size_offset = 0x3F0;
device.cart.cic_seed = 0x91; // CicNus6105
}
"36ADC40148AF56F0D78CD505EB6A90117D1FD6F11C6309E52ED36BC4C6BA340E" => {
device.cart.cic_type = device::cart::CicType::CicNus6106;
device.cart.cic_seed = 0x85;
device.cart.rdram_size_offset = 0x318;
device.cart.cic_seed = 0x85; // CicNus6106
}
"53C0088FB777870D0AF32F0251E964030E2E8B72E830C26042FD191169508C05" => {
device.cart.cic_type = device::cart::CicType::CicNus5167;
device.cart.cic_seed = 0xdd;
device.cart.rdram_size_offset = 0x318;
device.cart.cic_seed = 0xdd; // CicNus5167
device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] = 1;
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 0;
}
_ => {
device.cart.cic_type = device::cart::CicType::CicNus6102;
device.cart.cic_seed = 0x3F;
device.cart.rdram_size_offset = 0x318;
device.cart.cic_seed = 0x3F; // CicNus6102
println!("unknown IPL3 {}", hash)
}
}
+115 -22
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::ui;
pub const SDCARD_SIZE: usize = 0x4000000;
@@ -11,19 +12,22 @@ const SC64_KEY_REG: u32 = 4;
//const SC64_AUX_REG: u32 = 6;
pub const SC64_REGS_COUNT: u32 = 7;
pub const SC64_BOOTLOADER_SWITCH: u32 = 0;
pub const SC64_ROM_WRITE_ENABLE: u32 = 1;
pub const SC64_SAVE_TYPE: u32 = 6;
pub const SC64_CFG_COUNT: u32 = 15;
const SC64_BUFFER_MASK: usize = 0x1FFF;
const SC64_EEPROM_MASK: usize = 0xFFF;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Sc64 {
#[serde(with = "serde_big_array::BigArray")]
pub buffer: [u8; 8192],
pub buffer: Vec<u8>,
pub regs: [u32; SC64_REGS_COUNT as usize],
pub regs_locked: bool,
pub cfg: [u32; SC64_CFG_COUNT as usize],
pub sector: u32,
pub writeback_sector: Vec<u32>,
}
fn format_sdcard(device: &mut device::Device) {
@@ -74,6 +78,11 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
SC64_SCR_REG => {
if !device.cart.sc64.regs_locked {
match char::from_u32(value & mask).unwrap() {
'V' => {
// get version
device.cart.sc64.regs[SC64_DATA0_REG as usize] = (2 << 16) | 20;
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 2;
}
'c' => {
// get config
device.cart.sc64.regs[SC64_DATA1_REG as usize] = device.cart.sc64.cfg
@@ -81,6 +90,35 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
'C' => {
// set config
if device.cart.sc64.regs[SC64_DATA0_REG as usize] == SC64_SAVE_TYPE {
// if save type is being written, we are probably booting a game using the flash cart menu
// we shouldn't write saves to disk in this case (they are written to the SD card)
device.ui.saves.write_to_disk = false;
device.ui.save_type =
match device.cart.sc64.regs[SC64_DATA1_REG as usize] {
0 => {
vec![]
}
1 => {
vec![ui::storage::SaveTypes::Eeprom4k]
}
2 => {
vec![ui::storage::SaveTypes::Eeprom16k]
}
3 => {
vec![ui::storage::SaveTypes::Sram]
}
4 => {
vec![ui::storage::SaveTypes::Flash]
}
_ => {
panic!(
"unknown sc64 save type: {}",
device.cart.sc64.regs[SC64_DATA1_REG as usize]
)
}
}
}
std::mem::swap(
&mut device.cart.sc64.cfg
[device.cart.sc64.regs[SC64_DATA0_REG as usize] as usize],
@@ -165,8 +203,24 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
device.ui.saves.sdcard.written = true;
}
'U' => {} // USB_WRITE_STATUS, ignored
'M' => {} // USB_WRITE, ignored
'U' | 'u' => {} // USB status, ignored
'M' | 'm' => {} // USB read/write, ignored
'w' => {
// SD card writeback pending
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
}
'W' => {
let writeback_sectors_address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64;
for i in 0..256 {
device.cart.sc64.writeback_sector[i] = device::memory::data_read(
device,
writeback_sectors_address + (i * 4) as u64,
device::memory::AccessSize::Word,
false,
);
}
}
_ => {
panic!(
"unknown sc64 command: {}",
@@ -190,24 +244,48 @@ pub fn read_mem(
address: u64,
_access_size: device::memory::AccessSize,
) -> u32 {
let masked_address = address as usize & SC64_BUFFER_MASK;
u32::from_be_bytes(
device.cart.sc64.buffer[masked_address..masked_address + 4]
.try_into()
.unwrap(),
)
if address & 0x2000 != 0 {
device::cart::format_eeprom(device);
let masked_address = address as usize & SC64_EEPROM_MASK;
u32::from_be_bytes(
device.ui.saves.eeprom.data[masked_address..masked_address + 4]
.try_into()
.unwrap(),
)
} else {
let masked_address = address as usize & SC64_BUFFER_MASK;
u32::from_be_bytes(
device.cart.sc64.buffer[masked_address..masked_address + 4]
.try_into()
.unwrap(),
)
}
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let masked_address = address as usize & SC64_BUFFER_MASK;
let mut data = u32::from_be_bytes(
if address & 0x2000 != 0 {
device::cart::format_eeprom(device);
let masked_address = address as usize & SC64_EEPROM_MASK;
let mut data = u32::from_be_bytes(
device.ui.saves.eeprom.data[masked_address..masked_address + 4]
.try_into()
.unwrap(),
);
device::memory::masked_write_32(&mut data, value, mask);
device.ui.saves.eeprom.data[masked_address..masked_address + 4]
.copy_from_slice(&data.to_be_bytes());
device.ui.saves.eeprom.written = true
} else {
let masked_address = address as usize & SC64_BUFFER_MASK;
let mut data = u32::from_be_bytes(
device.cart.sc64.buffer[masked_address..masked_address + 4]
.try_into()
.unwrap(),
);
device::memory::masked_write_32(&mut data, value, mask);
device.cart.sc64.buffer[masked_address..masked_address + 4]
.try_into()
.unwrap(),
);
device::memory::masked_write_32(&mut data, value, mask);
device.cart.sc64.buffer[masked_address..masked_address + 4]
.copy_from_slice(&data.to_be_bytes());
.copy_from_slice(&data.to_be_bytes());
}
}
pub fn dma_read(
@@ -217,12 +295,20 @@ pub fn dma_read(
length: u32,
) -> u64 {
dram_addr &= device::rdram::RDRAM_MASK as u32;
cart_addr &= SC64_BUFFER_MASK as u32;
let buffer = if cart_addr & 0x2000 != 0 {
device::cart::format_eeprom(device);
cart_addr &= SC64_EEPROM_MASK as u32;
device.ui.saves.eeprom.written = true;
&mut device.ui.saves.eeprom.data
} else {
cart_addr &= SC64_BUFFER_MASK as u32;
&mut device.cart.sc64.buffer
};
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
device.cart.sc64.buffer[j as usize] = device.rdram.mem[i as usize ^ device.byte_swap];
buffer[j as usize] = device.rdram.mem[i as usize ^ device.byte_swap];
i += 1;
j += 1;
}
@@ -237,12 +323,19 @@ pub fn dma_write(
length: u32,
) -> u64 {
dram_addr &= device::rdram::RDRAM_MASK as u32;
cart_addr &= SC64_BUFFER_MASK as u32;
let buffer = if cart_addr & 0x2000 != 0 {
device::cart::format_eeprom(device);
cart_addr &= SC64_EEPROM_MASK as u32;
&device.ui.saves.eeprom.data
} else {
cart_addr &= SC64_BUFFER_MASK as u32;
&device.cart.sc64.buffer
};
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
device.rdram.mem[i as usize ^ device.byte_swap] = device.cart.sc64.buffer[j as usize];
device.rdram.mem[i as usize ^ device.byte_swap] = buffer[j as usize];
i += 1;
j += 1;
}
+9 -1
View File
@@ -1,5 +1,6 @@
use crate::{device, netplay, savestates, ui};
pub mod gbcart;
pub mod mempak;
pub mod rumble;
pub mod transferpak;
@@ -170,7 +171,14 @@ pub fn pak_switch_event(device: &mut device::Device) {
let new_pak_type = match channel.change_pak {
PakType::MemPak => PakType::RumblePak,
PakType::RumblePak => PakType::MemPak,
PakType::RumblePak => {
if device.transferpaks[i].cart.rom.is_empty() {
PakType::MemPak
} else {
PakType::TransferPak
}
}
PakType::TransferPak => PakType::MemPak,
_ => {
panic!("Invalid pak type");
}
+252
View File
@@ -0,0 +1,252 @@
use crate::device;
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub struct GbCart {
pub enabled: bool,
#[serde(skip)]
pub rom: Vec<u8>,
pub ram: Vec<u8>,
pub cart_type: device::controller::gbcart::CartType,
pub ram_enabled: bool,
pub ram_bank: u16,
pub rom_bank: u32,
pub set_latch: bool,
pub latch_second: u8,
pub latch_minute: u8,
pub latch_hour: u8,
pub latch_day: u16,
}
#[derive(Default, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum CartType {
#[default]
None,
MBC3RamBatt,
MBC3RamBattRtc,
MBC5RamBatt,
}
fn write_mbc3(
pif_ram: &mut [u8],
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
) {
let value = pif_ram[data + size - 1];
if address < 0x2000 {
cart.ram_enabled = value & 0xf == 0xa;
} else if address < 0x4000 {
let bank = value & 0x7f;
cart.rom_bank = bank as u32;
if cart.rom_bank == 0 {
cart.rom_bank = 1;
}
} else if address < 0x6000 {
cart.ram_bank = (value & 0xf) as u16;
} else if address < 0x8000 {
if !cart.set_latch && value != 0 {
// not implemented
}
cart.set_latch = value != 0;
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
return;
}
if cart.ram_bank < 0x8 {
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
cart.ram[banked_address as usize..banked_address as usize + size]
.copy_from_slice(&pif_ram[data..data + size]);
} else {
match cart.ram_bank {
0x8 => {
cart.latch_second = value;
}
0x9 => {
cart.latch_minute = value;
}
0xA => {
cart.latch_hour = value;
}
0xB => {
cart.latch_day &= 0xFF00;
cart.latch_day |= value as u16;
}
0xC => {
cart.latch_day &= 0x00FF;
cart.latch_day |= (value as u16) << 8;
}
_ => {
panic!("Unsupported ram bank {:x}", cart.ram_bank);
}
}
}
} else {
panic!("Unsupported write address {:x}", address);
}
}
fn read_mbc3(
pif_ram: &mut [u8],
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
) {
if address < 0x4000 {
let banked_address = address & 0x3FFF;
pif_ram[data..data + size]
.copy_from_slice(&cart.rom[banked_address as usize..banked_address as usize + size]);
} else if address < 0x8000 {
let banked_address = address as u32 - 0x4000 + (cart.rom_bank * 0x4000);
pif_ram[data..data + size]
.copy_from_slice(&cart.rom[banked_address as usize..banked_address as usize + size]);
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
for i in 0..size {
pif_ram[data + i] = 0xff;
}
return;
}
if cart.ram_bank < 0x8 {
if cart.ram_bank > 3 {
for i in 0..size {
pif_ram[data + i] = 0;
}
} else {
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
pif_ram[data..data + size].copy_from_slice(
&cart.ram[banked_address as usize..banked_address as usize + size],
);
}
} else {
let latch = match cart.ram_bank {
0x8 => cart.latch_second,
0x9 => cart.latch_minute,
0xA => cart.latch_hour,
0xB => cart.latch_day as u8,
0xC => (cart.latch_day >> 8) as u8,
_ => {
panic!("Unsupported ram bank {:x}", cart.ram_bank);
}
};
for i in 0..size {
pif_ram[data + i] = latch;
}
}
} else {
panic!("Unsupported read address {:x}", address);
}
}
fn write_mbc5(
pif_ram: &mut [u8],
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
) {
let value = pif_ram[data + size - 1];
if address < 0x2000 {
cart.ram_enabled = value & 0xf == 0xa;
} else if address < 0x3000 {
cart.rom_bank &= 0xff00;
cart.rom_bank |= value as u32;
} else if address < 0x4000 {
cart.rom_bank &= 0x00ff;
cart.rom_bank |= ((value & 0x1) as u32) << 8;
} else if address < 0x6000 {
cart.ram_bank = (value & 0xf) as u16;
} else if address < 0xa000 {
println!("Unknown MBC5 write address {:x}", address);
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
return;
}
let banked_address = address - 0xA000 + (cart.ram_bank << 13);
cart.ram[banked_address as usize..banked_address as usize + size]
.copy_from_slice(&pif_ram[data..data + size]);
} else {
panic!("Unsupported write address {:x}", address);
}
}
fn read_mbc5(
pif_ram: &mut [u8],
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
) {
if address < 0x4000 {
let banked_address = address & 0x3FFF;
pif_ram[data..data + size]
.copy_from_slice(&cart.rom[banked_address as usize..banked_address as usize + size]);
} else if address < 0x8000 {
let banked_address = address as u32 - 0x4000 + (cart.rom_bank << 14);
pif_ram[data..data + size]
.copy_from_slice(&cart.rom[banked_address as usize..banked_address as usize + size]);
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
for i in 0..size {
pif_ram[data + i] = 0xff;
}
return;
}
let banked_address = address - 0xA000 + (cart.ram_bank << 13);
pif_ram[data..data + size]
.copy_from_slice(&cart.ram[banked_address as usize..banked_address as usize + size]);
} else {
panic!("Unsupported read address {:x}", address);
}
}
pub fn read(
pif_ram: &mut [u8],
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
) {
if !cart.enabled {
for i in 0..size {
pif_ram[data + i] = 0x00;
}
return;
}
match cart.cart_type {
CartType::MBC3RamBatt => read_mbc3(pif_ram, cart, address, data, size),
CartType::MBC3RamBattRtc => read_mbc3(pif_ram, cart, address, data, size),
CartType::MBC5RamBatt => read_mbc5(pif_ram, cart, address, data, size),
_ => panic!("Unsupported cart type"),
}
}
pub fn write(
pif_ram: &mut [u8],
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
) {
if !cart.enabled {
return;
}
match cart.cart_type {
CartType::MBC3RamBatt => write_mbc3(pif_ram, cart, address, data, size),
CartType::MBC3RamBattRtc => write_mbc3(pif_ram, cart, address, data, size),
CartType::MBC5RamBatt => write_mbc5(pif_ram, cart, address, data, size),
_ => panic!("Unsupported cart type"),
}
}
pub fn get_cart_type(data: u8) -> CartType {
match data {
0x10 => CartType::MBC3RamBattRtc,
0x13 => CartType::MBC3RamBatt,
0x1b => CartType::MBC5RamBatt,
_ => panic!("Unsupported cart type {:x}", data),
}
}
+128 -14
View File
@@ -1,19 +1,133 @@
use crate::device;
pub fn read(
_device: &mut device::Device,
_channel: usize,
_address: u16,
_data: usize,
_size: usize,
) {
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub struct TransferPak {
pub enabled: bool,
pub reset_state: u8,
pub bank: u16,
pub cart: device::controller::gbcart::GbCart,
}
pub fn write(
_device: &mut device::Device,
_channel: usize,
_address: u16,
_data: usize,
_size: usize,
) {
pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
let pak = &mut device.transferpaks[channel];
match address >> 12 {
0x8 => {
let value = if pak.enabled { 0x84 } else { 0x00 };
for i in 0..size {
device.pif.ram[data + i] = value;
}
return;
}
_ => {
if !pak.enabled {
for i in 0..size {
device.pif.ram[data + i] = 0x00;
}
return;
}
}
}
match address >> 12 {
0xB => {
let mut value = 0;
if pak.cart.enabled {
value |= 1 << 0;
}
value |= (pak.reset_state & 3) << 2;
if pak.enabled {
value |= 1 << 7;
}
if pak.cart.enabled && pak.reset_state == 3 {
pak.reset_state = 2
} else if !pak.cart.enabled && pak.reset_state == 2 {
pak.reset_state = 1
} else if !pak.cart.enabled && pak.reset_state == 1 {
pak.reset_state = 0
}
for i in 0..size {
device.pif.ram[data + i] = value;
}
}
0xC..=0xF => {
device::controller::gbcart::read(
&mut device.pif.ram,
&mut pak.cart,
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
data,
size,
);
}
_ => {
panic!("unknown transfer pak read {:x}", address >> 12);
}
}
}
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
let pak = &mut device.transferpaks[channel];
let value = device.pif.ram[data + size - 1];
match address >> 12 {
0x8 => {
match value {
0xFE => {
pak.enabled = false;
}
0x84 => {
if !pak.enabled {
pak.bank = 3;
pak.cart.enabled = false;
pak.reset_state = 0;
}
pak.enabled = true;
}
_ => panic!("unknown transfer pak value"),
}
return;
}
_ => {
if !pak.enabled {
return;
}
}
}
match address >> 12 {
0xA => {
pak.bank = value as u16;
if pak.bank > 3 {
pak.bank = 0;
}
}
0xB => {
if value & 1 != 0 {
if !pak.cart.enabled {
pak.reset_state = 3;
pak.cart.rom_bank = 1;
pak.cart.ram_bank = 0;
pak.cart.ram_enabled = false;
pak.cart.cart_type =
device::controller::gbcart::get_cart_type(pak.cart.rom[0x147]);
}
pak.cart.enabled = true;
} else {
pak.cart.enabled = false;
}
}
0xC..=0xF => {
device::controller::gbcart::write(
&mut device.pif.ram,
&mut pak.cart,
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
data,
size,
);
}
_ => {
panic!("unknown transfer pak write {:x}", address >> 12);
}
}
}
+4 -4
View File
@@ -153,13 +153,13 @@ pub fn init(device: &mut device::Device) {
} else if (MM_DOM2_ADDR2 >> 16..=(MM_DOM2_ADDR2 + 0x1FFFF) >> 16).contains(&i) {
device.memory.memory_map_read[i] = device::cart::sram::read_mem;
device.memory.memory_map_write[i] = device::cart::sram::write_mem;
} else if i >= MM_CART_ROM >> 16 && i <= (MM_CART_ROM + device.cart.rom.len() - 1) >> 16 {
device.memory.fast_read[i] = device::cart::rom::read_mem_fast;
device.memory.memory_map_read[i] = device::cart::rom::read_mem;
device.memory.memory_map_write[i] = device::cart::rom::write_mem;
} else if i >= MM_IS_VIEWER >> 16 && i <= (MM_IS_VIEWER + 0xFFFF) >> 16 {
device.memory.memory_map_read[i] = device::is_viewer::read_mem;
device.memory.memory_map_write[i] = device::is_viewer::write_mem;
} else if (MM_CART_ROM >> 16..=(MM_CART_ROM + 0x0FBFFFFF) >> 16).contains(&i) {
device.memory.fast_read[i] = device::cart::rom::read_mem_fast;
device.memory.memory_map_read[i] = device::cart::rom::read_mem;
device.memory.memory_map_write[i] = device::cart::rom::write_mem;
} else if i >= MM_PIF_MEM >> 16 && i <= (MM_PIF_MEM + 0xFFFF) >> 16 {
device.memory.memory_map_read[i] = device::pif::read_mem;
device.memory.memory_map_write[i] = device::pif::write_mem;
+11 -2
View File
@@ -55,7 +55,7 @@ pub fn read_mem(
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let mut masked_address = address as usize & PIF_MASK;
if masked_address < PIF_RAM_OFFSET {
panic! {"write to pif rom"}
panic!("write to pif rom")
}
masked_address -= PIF_RAM_OFFSET;
let mut data = u32::from_be_bytes(
@@ -250,13 +250,22 @@ pub fn init(device: &mut device::Device) {
write: device::controller::mempak::write,
pak_type: device::controller::PakType::MemPak,
};
let tpak_handler = device::controller::PakHandler {
read: device::controller::transferpak::read,
write: device::controller::transferpak::write,
pak_type: device::controller::PakType::TransferPak,
};
connect_pif_channels(device);
for i in 0..4 {
if device.netplay.is_none() {
if device.ui.config.input.controller_enabled[i] {
device.pif.channels[i].pak_handler = Some(mempak_handler);
if device.transferpaks[i].cart.rom.is_empty() {
device.pif.channels[i].pak_handler = Some(mempak_handler);
} else {
device.pif.channels[i].pak_handler = Some(tpak_handler);
}
}
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
device.pif.channels[i].pak_handler = Some(mempak_handler);
+3 -4
View File
@@ -106,13 +106,12 @@ pub fn init(device: &mut device::Device) {
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
// hack, skip RDRAM initialization
device.rdram.mem[device.cart.rdram_size_offset..device.cart.rdram_size_offset + 4]
.copy_from_slice(&device.rdram.size.to_ne_bytes());
device.rdram.mem[0x318..0x318 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
// hack, skip RDRAM initialization
device.rdram.mem[0x3f0..0x3f0 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
device.ri.regs[device::ri::RI_MODE_REG as usize] = 0x0e;
device.ri.regs[device::ri::RI_CONFIG_REG as usize] = 0x40;
device.ri.regs[device::ri::RI_SELECT_REG as usize] = 0x14;
device.ri.regs[device::ri::RI_REFRESH_REG as usize] = 0x00063634;
}
pub fn rdram_calculate_cycles(length: u64) -> u64 {
+16 -9
View File
@@ -22,17 +22,24 @@ pub fn read_regs(
_access_size: device::memory::AccessSize,
) -> u32 {
device::cop0::add_cycles(device, 20);
if ((address & 0xFFFF) >> 2) as u32 == RI_SELECT_REG && !device.ri.ram_init {
device::cop0::add_cycles(device, device.cpu.clock_rate / 2); // hack, simulate RDRAM initialization
device.ri.ram_init = true;
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
RI_SELECT_REG => {
if !device.ri.ram_init {
device::cop0::add_cycles(device, device.cpu.clock_rate / 2); // hack, simulate RDRAM initialization
device.ri.ram_init = true;
}
0x14 // hack, skip RDRAM initialization
}
RI_REFRESH_REG => 0x00063634, // hack, skip RDRAM initialization
_ => device.ri.regs[reg as usize],
}
device.ri.regs[((address & 0xFFFF) >> 2) as usize]
}
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
device::memory::masked_write_32(
&mut device.ri.regs[((address & 0xFFFF) >> 2) as usize],
value,
mask,
);
let reg = (address & 0xFFFF) >> 2;
if reg as u32 == RI_SELECT_REG {
device.ri.ram_init = false;
}
device::memory::masked_write_32(&mut device.ri.regs[reg as usize], value, mask);
}
+13 -1
View File
@@ -169,8 +169,20 @@ async fn main() {
handle.join().unwrap();
} else {
let options = eframe::NativeOptions {
wgpu_options: eframe::egui_wgpu::WgpuConfiguration {
wgpu_setup: eframe::egui_wgpu::WgpuSetup::CreateNew(
eframe::egui_wgpu::WgpuSetupCreateNew {
instance_descriptor: eframe::egui_wgpu::wgpu::InstanceDescriptor {
backends: eframe::wgpu::Backends::GL,
..Default::default()
},
..Default::default()
},
),
..Default::default()
},
viewport: eframe::egui::ViewportBuilder::default()
.with_inner_size([640.0, 480.0])
.with_inner_size([854.0, 480.0])
.with_icon(
eframe::icon_data::from_png_bytes(include_bytes!("../data/gopher64.png"))
.unwrap(),
+19 -1
View File
@@ -16,6 +16,8 @@ const TCP_REGISTER_PLAYER: u8 = 5;
const TCP_GET_REGISTRATION: u8 = 6;
const TCP_DISCONNECT_NOTICE: u8 = 7;
const TCP_RECEIVE_SAVE_WITH_SIZE: u8 = 8;
const TCP_SEND_RTC: u8 = 64;
const TCP_RECEIVE_RTC: u8 = 128;
const CS4: u32 = 32;
@@ -50,6 +52,22 @@ pub const NETPLAY_ERROR_PLAYER_2_DISCONNECTED: u32 = 4;
pub const NETPLAY_ERROR_PLAYER_3_DISCONNECTED: u32 = 5;
pub const NETPLAY_ERROR_PLAYER_4_DISCONNECTED: u32 = 6;
pub fn send_rtc(netplay: &mut Netplay, rtc: i64) {
let mut request: Vec<u8> = [TCP_SEND_RTC].to_vec();
let size: u32 = 8;
request.extend_from_slice(&size.to_be_bytes());
request.extend_from_slice(&rtc.to_be_bytes());
netplay.tcp_stream.write_all(&request).unwrap();
}
pub fn receive_rtc(netplay: &mut Netplay) -> i64 {
let request: Vec<u8> = [TCP_RECEIVE_RTC].to_vec();
netplay.tcp_stream.write_all(&request).unwrap();
let mut rtc: [u8; 8] = [0; 8];
netplay.tcp_stream.read_exact(&mut rtc).unwrap();
i64::from_be_bytes(rtc)
}
pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8], size: usize) {
let mut request: Vec<u8> = [TCP_SEND_SAVE].to_vec();
request.extend_from_slice(save_type.as_bytes());
@@ -204,7 +222,7 @@ fn process_incoming(netplay: &mut Netplay, ui: &mut ui::Ui) {
}
}
_ => {
panic! {"unknown UDP packet"}
panic!("unknown UDP packet")
}
}
}
+18
View File
@@ -108,6 +108,15 @@ pub fn load_savestate(device: &mut device::Device) {
device.ri = state.ri;
device.vru = state.vru;
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
for (i, item) in tpak_rom.iter_mut().enumerate() {
*item = device.transferpaks[i].cart.rom.clone();
}
device.transferpaks = state.transferpaks;
for (i, item) in tpak_rom.iter().enumerate() {
device.transferpaks[i].cart.rom = item.clone();
}
device::memory::init(device);
device::vi::set_expected_refresh_rate(device);
device::cpu::map_instructions(device);
@@ -166,6 +175,15 @@ pub fn load_savestate(device: &mut device::Device) {
pak_type: device::controller::PakType::MemPak,
};
device.pif.channels[i].pak_handler = Some(mempak_handler);
} else if device.pif.channels[i].pak_handler.unwrap().pak_type
== device::controller::PakType::TransferPak
{
let tpak_handler = device::controller::PakHandler {
read: device::controller::transferpak::read,
write: device::controller::transferpak::write,
pak_type: device::controller::PakType::TransferPak,
};
device.pif.channels[i].pak_handler = Some(tpak_handler);
}
}
}
+1
View File
@@ -122,6 +122,7 @@ impl Ui {
savestate_file_path: std::path::PathBuf::new(),
},
saves: storage::Saves {
write_to_disk: true,
eeprom: storage::Save {
data: Vec::new(),
written: false,
+8 -3
View File
@@ -14,15 +14,20 @@ pub struct EventAudio {
pub fn init(ui: &mut ui::Ui, frequency: u64) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
let audio_spec = sdl3_sys::audio::SDL_AudioSpec {
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
channels: 2,
};
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 48000,
channels: 2,
};
ui.audio_device = unsafe {
sdl3_sys::audio::SDL_OpenAudioDevice(
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&audio_spec,
&device_audio_spec,
)
};
if ui.audio_device == 0 {
@@ -36,7 +41,7 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
panic!("Could not get audio device format");
}
ui.audio_stream = unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&audio_spec, &dst) };
ui.audio_stream = unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &dst) };
if ui.audio_stream.is_null() {
return;
}
+2
View File
@@ -37,6 +37,7 @@ pub struct Input {
pub input_profile_binding: [String; 4],
pub controller_assignment: [Option<String>; 4],
pub controller_enabled: [bool; 4],
pub transfer_pak: [bool; 4],
pub emulate_vru: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -88,6 +89,7 @@ impl Config {
controller_assignment: [None, None, None, None],
input_profiles,
controller_enabled: [true, false, false, false],
transfer_pak: [false, false, false, false],
emulate_vru: false,
},
video: Video {
+148 -10
View File
@@ -12,6 +12,7 @@ pub struct GopherEguiApp {
selected_profile: [String; 4],
input_profiles: Vec<String>,
controller_enabled: [bool; 4],
transfer_pak: [bool; 4],
controller_paths: Vec<String>,
upscale: u32,
integer_scaling: bool,
@@ -22,13 +23,21 @@ pub struct GopherEguiApp {
vru_window_receiver: Option<tokio::sync::mpsc::Receiver<Vec<String>>>,
vru_word_notifier: Option<tokio::sync::mpsc::Sender<String>>,
vru_word_list: Vec<String>,
latest_version: Option<semver::Version>,
update_receiver: Option<tokio::sync::mpsc::Receiver<GithubData>>,
pub netplay: gui_netplay::GuiNetplay,
}
#[derive(serde::Deserialize)]
struct GithubData {
tag_name: String,
}
struct SaveConfig {
selected_controller: [i32; 4],
selected_profile: [String; 4],
controller_enabled: [bool; 4],
transfer_pak: [bool; 4],
upscale: u32,
integer_scaling: bool,
fullscreen: bool,
@@ -103,6 +112,7 @@ impl GopherEguiApp {
controller_names,
input_profiles: get_input_profiles(&config),
controller_enabled: config.input.controller_enabled,
transfer_pak: config.input.transfer_pak,
upscale: config.video.upscale,
integer_scaling: config.video.integer_scaling,
fullscreen: config.video.fullscreen,
@@ -112,6 +122,8 @@ impl GopherEguiApp {
controller_paths,
vru_window_receiver: None,
vru_word_notifier: None,
latest_version: None,
update_receiver: None,
vru_word_list: Vec::new(),
netplay: Default::default(),
dirs: ui::get_dirs(),
@@ -135,6 +147,7 @@ fn save_config(
config.input.input_profile_binding = save_config_items.selected_profile;
config.input.controller_enabled = save_config_items.controller_enabled;
config.input.transfer_pak = save_config_items.transfer_pak;
config.video.upscale = save_config_items.upscale;
config.video.integer_scaling = save_config_items.integer_scaling;
@@ -148,6 +161,7 @@ impl Drop for GopherEguiApp {
selected_controller: self.selected_controller,
selected_profile: self.selected_profile.clone(),
controller_enabled: self.controller_enabled,
transfer_pak: self.transfer_pak,
upscale: self.upscale,
integer_scaling: self.integer_scaling,
fullscreen: self.fullscreen,
@@ -237,12 +251,52 @@ fn show_vru_dialog(app: &mut GopherEguiApp, ctx: &egui::Context) {
);
}
fn get_latest_version(app: &mut GopherEguiApp, ctx: &egui::Context) {
if app.update_receiver.is_none() {
let (tx, rx) = tokio::sync::mpsc::channel(1);
app.update_receiver = Some(rx);
let gui_ctx = ctx.clone();
let client = reqwest::Client::builder()
.user_agent(env!("CARGO_PKG_NAME"))
.build()
.unwrap();
let task = client
.get("https://api.github.com/repos/gopher64/gopher64/releases/latest")
.send();
tokio::spawn(async move {
let response = task.await;
if let Ok(response) = response {
let data: Result<GithubData, reqwest::Error> = response.json().await;
if data.is_ok() {
tx.send(data.unwrap()).await.unwrap();
} else {
tx.send(GithubData {
tag_name: format!("v{}", env!("CARGO_PKG_VERSION")),
})
.await
.unwrap();
}
gui_ctx.request_repaint();
}
});
} else if app.latest_version.is_none() {
let result = app.update_receiver.as_mut().unwrap().try_recv();
if result.is_ok() {
let tag = &result.unwrap().tag_name[1..];
app.latest_version = Some(semver::Version::parse(tag).unwrap());
} else {
ctx.request_repaint();
}
}
}
pub fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) {
let netplay;
let selected_controller = app.selected_controller;
let selected_profile = app.selected_profile.clone();
let controller_enabled = app.controller_enabled;
let transfer_pak = app.transfer_pak;
let upscale = app.upscale;
let integer_scaling = app.integer_scaling;
let fullscreen = app.fullscreen;
@@ -286,12 +340,47 @@ pub fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) {
let rom_contents = app.netplay.rom_contents.clone();
let gui_ctx = ctx.clone();
let mut task = None;
let mut select_rom = None;
let mut select_gb_rom = [None, None, None, None];
let mut select_gb_ram = [None, None, None, None];
if !netplay {
task = Some(rfd::AsyncFileDialog::new().pick_file());
select_rom = Some(
rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file(),
);
for i in 0..4 {
if transfer_pak[i] {
select_gb_rom[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB ROM P{}", i + 1))
.pick_file(),
);
select_gb_ram[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB RAM P{}", i + 1))
.pick_file(),
);
}
}
}
tokio::spawn(async move {
let file = if !netplay { task.unwrap().await } else { None };
let file = if !netplay {
select_rom.unwrap().await
} else {
None
};
let mut gb_rom_path = [None, None, None, None];
let mut gb_ram_path = [None, None, None, None];
if !netplay {
for i in 0..4 {
if transfer_pak[i] {
gb_rom_path[i] = select_gb_rom[i].as_mut().unwrap().await;
gb_ram_path[i] = select_gb_ram[i].as_mut().unwrap().await;
}
}
}
std::thread::Builder::new()
.name("n64".to_string())
@@ -301,6 +390,7 @@ pub fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) {
selected_controller,
selected_profile,
controller_enabled,
transfer_pak,
upscale,
integer_scaling,
fullscreen,
@@ -331,6 +421,16 @@ pub fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) {
device::run_game(rom_contents, &mut device);
netplay::close(&mut device);
} else {
for i in 0..4 {
if gb_rom_path[i].is_some() && gb_ram_path[i].is_some() {
device.transferpaks[i].cart.rom =
std::fs::read(gb_rom_path[i].as_ref().unwrap().path()).unwrap();
device.transferpaks[i].cart.ram =
std::fs::read(gb_ram_path[i].as_ref().unwrap().path()).unwrap();
}
}
if emulate_vru {
device.vru_window.window_notifier = Some(vru_window_notifier);
device.vru_window.word_receiver = Some(vru_word_receiver);
@@ -393,6 +493,19 @@ impl eframe::App for GopherEguiApp {
self.netplay.create = true;
}
if ui.button("Open Saves Folder").clicked() {
let command = if cfg!(target_os = "windows") {
"explorer"
} else if cfg!(target_os = "linux") {
"xdg-open"
} else {
panic!("Unsupported platform");
};
let _ = std::process::Command::new(command)
.arg(self.dirs.data_dir.join("saves"))
.spawn();
}
ui.end_row();
if ui.button("Configure Input Profile").clicked()
@@ -413,12 +526,28 @@ impl eframe::App for GopherEguiApp {
egui::Grid::new("controller_config").show(ui, |ui| {
ui.label("Port");
ui.label("Enabled");
ui.label("Emulate VRU");
ui.label("Transfer Pak");
ui.label("Profile");
ui.label("Controller");
ui.end_row();
for i in 0..4 {
ui.label(format!("{}", i + 1));
ui.checkbox(&mut self.controller_enabled[i], "");
ui.centered_and_justified(|ui| {
ui.checkbox(&mut self.controller_enabled[i], "");
});
let mut vru = false;
ui.centered_and_justified(|ui| {
if i < 3 {
ui.add_enabled(false, egui::Checkbox::new(&mut vru, ""));
} else {
ui.add_enabled(true, egui::Checkbox::new(&mut self.emulate_vru, ""));
}
});
ui.centered_and_justified(|ui| {
ui.checkbox(&mut self.transfer_pak[i], "");
});
egui::ComboBox::from_id_salt(format!("profile-combo-{}", i))
.selected_text(self.selected_profile[i].clone())
@@ -477,15 +606,22 @@ impl eframe::App for GopherEguiApp {
};
ui.checkbox(&mut self.integer_scaling, "Integer Scaling");
ui.checkbox(&mut self.fullscreen, "Fullscreen (Esc closes game)");
ui.add_space(32.0);
ui.checkbox(
&mut self.emulate_vru,
"Emulate VRU (connects VRU to controller port 4)",
);
ui.add_space(16.0);
ui.hyperlink_to("Wiki", "https://github.com/gopher64/gopher64/wiki");
ui.hyperlink_to("Discord Server", "https://discord.gg/9RGXq8W8JQ");
ui.add_space(16.0);
ui.add_space(32.0);
ui.label(format!("Version: {}", env!("CARGO_PKG_VERSION")));
if self.latest_version.is_some() {
let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
if current_version < *self.latest_version.as_ref().unwrap() {
ui.hyperlink_to(
"New version available!",
"https://github.com/gopher64/gopher64/releases/latest",
);
}
}
});
if self.emulate_vru && self.vru_window_receiver.is_some() {
@@ -499,6 +635,8 @@ impl eframe::App for GopherEguiApp {
if self.show_vru_dialog {
show_vru_dialog(self, ctx);
}
get_latest_version(self, ctx);
}
}
+6 -2
View File
@@ -207,7 +207,9 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) {
app.netplay.game_info_receiver = Some(rx);
let gui_ctx = ctx.clone();
app.netplay.create_rom_label = "Inspecting ROM".to_string();
let task = rfd::AsyncFileDialog::new().pick_file();
let task = rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file();
tokio::spawn(async move {
let file = task.await;
@@ -621,7 +623,9 @@ pub fn netplay_join(app: &mut GopherEguiApp, ctx: &egui::Context) {
app.netplay.game_info_receiver = Some(rx);
let gui_ctx = ctx.clone();
app.netplay.join_rom_label = "Inspecting ROM".to_string();
let task = rfd::AsyncFileDialog::new().pick_file();
let task = rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file();
tokio::spawn(async move {
let file = task.await;
+54 -17
View File
@@ -1,4 +1,4 @@
use crate::{netplay, ui};
use crate::{device, netplay, ui};
use std::io::Read;
use std::io::Write;
@@ -45,6 +45,7 @@ pub struct Saves {
pub mempak: Save,
pub sdcard: Save,
pub romsave: RomSave,
pub write_to_disk: bool,
}
fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
@@ -317,25 +318,61 @@ fn write_rom_save(ui: &ui::Ui) {
std::fs::write(ui.paths.romsave_file_path.clone(), data).unwrap();
}
pub fn write_saves(ui: &ui::Ui, netplay: &Option<netplay::Netplay>) {
if netplay.is_none() || netplay.as_ref().unwrap().player_number == 0 {
if ui.saves.eeprom.written {
write_save(ui, SaveTypes::Eeprom16k)
fn writeback_sdcard(device: &mut device::Device) {
let length;
let save_data: &[u8];
if device.ui.saves.eeprom.written {
if device
.ui
.save_type
.contains(&ui::storage::SaveTypes::Eeprom4k)
{
length = 1;
} else {
length = 4;
}
if ui.saves.sram.written {
write_save(ui, SaveTypes::Sram)
save_data = device.ui.saves.eeprom.data.as_ref();
} else if device.ui.saves.sram.written {
length = device.ui.saves.sram.data.len() / 512;
save_data = device.ui.saves.sram.data.as_ref();
} else if device.ui.saves.flash.written {
length = device.ui.saves.flash.data.len() / 512;
save_data = device.ui.saves.flash.data.as_ref();
} else {
return;
}
for i in 0..length {
let offset = device.cart.sc64.writeback_sector[i] as usize * 512;
device.ui.saves.sdcard.data[offset..offset + 512]
.copy_from_slice(&save_data[i * 512..(i + 1) * 512]);
}
device.ui.saves.sdcard.written = true;
}
pub fn write_saves(device: &mut device::Device) {
if device.netplay.is_none() || device.netplay.as_ref().unwrap().player_number == 0 {
if device.ui.saves.write_to_disk {
if device.ui.saves.eeprom.written {
write_save(&device.ui, SaveTypes::Eeprom16k)
}
if device.ui.saves.sram.written {
write_save(&device.ui, SaveTypes::Sram)
}
if device.ui.saves.flash.written {
write_save(&device.ui, SaveTypes::Flash)
}
if device.ui.saves.romsave.written {
write_save(&device.ui, SaveTypes::Romsave)
}
} else {
writeback_sdcard(device)
}
if ui.saves.flash.written {
write_save(ui, SaveTypes::Flash)
if device.ui.saves.mempak.written {
write_save(&device.ui, SaveTypes::Mempak)
}
if ui.saves.mempak.written {
write_save(ui, SaveTypes::Mempak)
}
if ui.saves.sdcard.written {
write_save(ui, SaveTypes::Sdcard)
}
if ui.saves.romsave.written {
write_save(ui, SaveTypes::Romsave)
if device.ui.saves.sdcard.written {
write_save(&device.ui, SaveTypes::Sdcard)
}
}
}