Compare commits

...

296 Commits

Author SHA1 Message Date
Logan McNaughton 81cc78ce90 bump crate and rust versions (#1155) 2026-07-09 13:06:49 +00:00
PolygonPrincess e8a7abffe3 Add cli flag for widescreen (#1152)
* Add cli flag for widescreen and enable config fallback

* Fix else statement brace spacing and trailing whitespace

* Fix trailing whitespace within args.widescreen else statement
2026-07-06 12:54:51 +00:00
dependabot[bot] f7ad6a4b0d Bump rust-toolchain from 1.96.0 to 1.96.1 (#1151)
Bumps [rust-toolchain](https://github.com/rust-lang/rust) from 1.96.0 to 1.96.1.
- [Release notes](https://github.com/rust-lang/rust/releases)
- [Changelog](https://github.com/rust-lang/rust/blob/main/RELEASES.md)
- [Commits](https://github.com/rust-lang/rust/compare/1.96.0...1.96.1)

---
updated-dependencies:
- dependency-name: rust-toolchain
  dependency-version: 1.96.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 04:34:17 +00:00
Logan McNaughton 1bfb50de8a re-arrange input profiles (#1150) 2026-07-05 10:43:23 +00:00
Logan McNaughton 3bc7c96191 move input polling (#1145)
* move input polling

* more

* more

* more
2026-07-02 08:04:51 +00:00
Logan McNaughton 032bf1acf3 Update contribution guidelines (#1143) 2026-07-01 07:58:17 +00:00
Logan McNaughton 8bc765c1a3 match RA branding softcore->casual (#1142) 2026-07-01 07:34:48 +00:00
Logan McNaughton da061240af normalize no-intro names (#1139) 2026-06-30 10:32:43 +00:00
Logan McNaughton 407a1b967c Re-arrange settings (#1138)
* update setting locations

* tra
2026-06-30 09:53:03 +00:00
999sian ad756cd6bd settings: add a dark/light theme toggle (#1132)
* settings: add dark/light theme toggle

* i18n: translate theme selector strings

* settings: align Resolution and Theme dropdowns in a GridBox
2026-06-30 09:17:12 +00:00
Logan McNaughton 6e11bc18d6 translation tweaks (#1137) 2026-06-30 05:24:48 +00:00
Logan McNaughton c8eee9f727 don't copy from db (#1136) 2026-06-30 05:23:38 +00:00
Logan McNaughton 01a53ce1c0 use no-intro names in netplay/cheats (#1133)
* use no-intro names in netplay/cheats

* more

* more
2026-06-29 19:37:33 +00:00
Logan McNaughton 0eddf31f92 update contributing guidelines (#1130) 2026-06-29 09:53:00 +00:00
Logan McNaughton e257ba717b update ggrs (#1127) 2026-06-27 08:19:14 +00:00
Logan McNaughton 7a797165fd Fix translation for 'Open Saves Folder' in Portuguese (#1125) 2026-06-26 07:23:50 +00:00
Logan McNaughton a659413090 bump ver (#1124) 2026-06-25 13:09:55 +00:00
Logan McNaughton 08e7ec71d9 clippy fix (#1123)
* clippy fix

* more
2026-06-25 13:09:18 +00:00
Logan McNaughton c97f14980b show proper number of players (#1121) 2026-06-25 12:28:09 +00:00
Logan McNaughton 5d090f496b update ggrs (#1119) 2026-06-25 06:41:33 +00:00
Logan McNaughton d6b46037b4 update netplay connection (#1118) 2026-06-24 20:44:38 +00:00
Logan McNaughton 5b5acfdd25 update slint (#1117)
* update slint

* more
2026-06-24 15:04:14 +00:00
Logan McNaughton 926338ba62 fix desync issue (#1116)
* fix desync issue

* more

* more

* more

* more
2026-06-24 12:52:56 +00:00
Logan McNaughton cc420013f9 add sleep while waiting for state (#1115)
* add sleep while waiting for state

* more

* more
2026-06-23 16:37:14 +00:00
Logan McNaughton ba0a21feca register tx early (#1114) 2026-06-23 14:52:27 +00:00
Logan McNaughton 70b9d20c64 fix android build (#1113)
* fix android build

* more

* more
2026-06-23 13:57:52 +00:00
Logan McNaughton dee2b85daa use tokio mutex (#1112)
* use tokio mutex

* more
2026-06-23 10:27:15 +00:00
Logan McNaughton 119e9044a8 cargo update (#1111) 2026-06-23 05:35:05 +00:00
dependabot[bot] e6a260bd6a Bump actions/checkout from 6 to 7 (#1108)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 05:00:57 +00:00
Logan McNaughton a23ede433b bump version (#1107) 2026-06-21 19:13:16 +00:00
Logan McNaughton 346b0a139d Clarify netplay support (#1106)
Updated netplay section to clarify support for P2P connections and added a note to read the netplay guide.
2026-06-21 15:12:19 +00:00
Logan McNaughton f5cda2901f clear join sessions list (#1105) 2026-06-21 14:22:21 +00:00
Logan McNaughton 2dda5c8706 handle matchbox panics (#1104) 2026-06-21 08:51:32 +00:00
Logan McNaughton d6aa2ddce3 send disconnect notice (#1103)
* send disconnect notice

* more

* more

* more

* more
2026-06-20 16:40:56 +00:00
Logan McNaughton 6f9801a185 cargo update (#1102) 2026-06-20 11:20:11 +00:00
Logan McNaughton c82edae338 show message more frequently (#1101)
* show message more frequently

* more
2026-06-20 07:58:25 +00:00
Logan McNaughton 57327c1f73 improve netplay startup (#1100) 2026-06-19 21:06:05 +00:00
Logan McNaughton e3a22bf7d0 tweak connection times (#1099)
* tweak connection times

* more

* more
2026-06-19 07:26:39 +00:00
Logan McNaughton 3718d914fd try to restore input data from old config (#1097) 2026-06-17 06:58:44 +00:00
Logan McNaughton 97a8b63c9c fix disconnect (#1096) 2026-06-16 16:46:57 +00:00
Logan McNaughton d6d0b66078 bump version (#1095) 2026-06-16 16:41:13 +00:00
Logan McNaughton b173598320 reset timeout (#1094) 2026-06-16 14:45:04 +00:00
Logan McNaughton f211eede3b retry socket creation (#1093)
* retry sockey creation

* more

* more

* more

* more
2026-06-16 14:22:53 +00:00
Logan McNaughton b123ed9fa3 input delay cannot be less than 1 (#1092) 2026-06-16 14:02:02 +00:00
Logan McNaughton 2b0d8e5423 skip save states (#1091)
* skip save states

* more
2026-06-16 13:34:18 +00:00
Logan McNaughton 49b8024fbe Revert translation changes (#1090)
* revert translation changes

* more
2026-06-16 13:05:58 +00:00
Logan McNaughton be987e1434 don't spam errors (#1089) 2026-06-16 12:58:16 +00:00
Logan McNaughton 27e2b89fcf re-arrange netplay setup (#1088)
* re-arrange netplay setup

* more

* more

* more

* more

* more

* more

* more

* more
2026-06-16 12:48:31 +00:00
Logan McNaughton c261a746a3 enable rollback (#1087)
* enable rollback

* more
2026-06-16 10:43:50 +00:00
Logan McNaughton c290e60af1 put tokio runtime in dedicated thread (#1086)
* put tokio runtime in dedicated thread

* more

* more

* more

* more
2026-06-16 09:33:27 +00:00
Logan McNaughton 19142a30c6 use unbounded channel (#1084)
* use unbounded channel

* more
2026-06-15 17:44:20 +00:00
Logan McNaughton 2ca317d742 put rec back (#1083) 2026-06-15 15:56:53 +00:00
Logan McNaughton 45963aeea5 Fix ping recommendation (#1082)
* don't block tokio

* more

* more

* more

* more

* more
2026-06-15 15:29:51 +00:00
Logan McNaughton 4929f2cc51 remove highest ping (#1081) 2026-06-15 13:02:29 +00:00
Logan McNaughton 90e51f5f09 setup callbacks at start (#1080)
* setup callbacks at start

* more

* more
2026-06-15 09:44:33 +00:00
Logan McNaughton 9b7aeccd48 close window on user request (#1079)
* close window on user request

* more

* more

* more
2026-06-15 06:53:54 +00:00
Logan McNaughton 10d55bd25c move netplay init (#1078)
* move netplay init

* more
2026-06-14 20:36:54 +00:00
Logan McNaughton 16224cd5a6 use spinbox for latency (#1077)
* use spinbox for latency

* more

* more
2026-06-14 18:28:36 +00:00
Logan McNaughton d62d4c852a Increase timeout duration from 2 to 3 seconds (#1075) 2026-06-14 10:50:35 +00:00
Logan McNaughton 6d6349c79d Increase timeout duration from 2 to 3 seconds (#1074) 2026-06-14 08:27:11 +00:00
Logan McNaughton 9d2eec1732 bump version (#1073) 2026-06-14 06:53:05 +00:00
Logan McNaughton d6da1656b8 clean up ICE config (#1072) 2026-06-13 17:29:36 +00:00
Logan McNaughton ae7bac65d5 use custom ICE server config (#1071)
* use custom ICE server config

* more

* more

* more

* more

* more

* more
2026-06-13 16:28:33 +00:00
Logan McNaughton 99c4d6dff8 average top 3 values (#1070) 2026-06-13 13:07:10 +00:00
Logan McNaughton 74600b4b25 pump sdl events before callbacks (#1069) 2026-06-13 13:00:12 +00:00
Logan McNaughton 6cae2ed3ac dont use usize in netplay messages (#1068) 2026-06-13 09:55:12 +00:00
Logan McNaughton 611b56186d update recommendation formula (#1067)
* update recommendation formula

* more
2026-06-13 09:14:43 +00:00
Logan McNaughton dfa673e6e9 add note about input delay (#1066)
* add note about input delay

* more
2026-06-13 08:00:29 +00:00
Logan McNaughton be74e3358a Use mpsc instead of mutex (#1065)
* use mpsc

* more

* more

* more
2026-06-12 14:16:06 +00:00
Logan McNaughton 4cc85d595f pass channel to ggrs (#1064)
* pass channel to ggrs

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-06-12 13:34:21 +00:00
Logan McNaughton b2d7a12033 wait for mesh (#1063)
* wait for mesh

* more

* more
2026-06-12 07:02:53 +00:00
Logan McNaughton 65cffbba0c cap recommendation (#1062) 2026-06-12 05:36:56 +00:00
Logan McNaughton 5f8e3b9760 run netplay_close on window close (#1061)
* run netplay_close on window close

* more
2026-06-11 19:16:06 +00:00
Logan McNaughton 397a15349c handle close messages (#1060) 2026-06-11 12:53:00 +00:00
Logan McNaughton 235a4af0a9 check number of peers (#1059) 2026-06-11 07:38:05 +00:00
Logan McNaughton b171d48f87 print errors (#1058) 2026-06-11 05:36:00 +00:00
Logan McNaughton bfeba94e20 show error (#1057) 2026-06-11 05:29:28 +00:00
Logan McNaughton aa0bde0999 use tls-vendored (#1056) 2026-06-11 05:16:13 +00:00
Logan McNaughton 72e4a6b94a early rollback code (#1054)
* some rollback test

* more
2026-06-10 15:09:07 +00:00
Logan McNaughton 88f00f5638 clean up if statement (#1053) 2026-06-10 12:07:10 +00:00
Logan McNaughton db21800d9f move rewind check (#1051)
* move rewind check

* more
2026-06-10 10:54:39 +00:00
Logan McNaughton 4a63f469bf update VI on V_START (#1050)
* update VI on V_START

* more

* more

* more

* more
2026-06-10 08:55:51 +00:00
Logan McNaughton 810852997a improve ping loop (#1049) 2026-06-09 20:27:12 +00:00
Logan McNaughton 02d53beeac bump ver (#1048) 2026-06-09 17:11:49 +00:00
Logan McNaughton d589d8bac2 improve loop management (#1047)
* improve loop management

* more
2026-06-09 16:56:56 +00:00
Logan McNaughton 0deb1d11fc fix some netplay state (#1046) 2026-06-09 16:09:11 +00:00
Logan McNaughton 6cc6153abb improve wait room (#1045) 2026-06-09 14:47:16 +00:00
Logan McNaughton af163dac62 Fixed input delay (#1044)
* disable rollback

* more

* more

* more

* more

* more

* more
2026-06-09 14:10:49 +00:00
Logan McNaughton 8c4be03ab7 Revert "move netplay poll (#1042)" (#1043)
This reverts commit fce57f8ee2.
2026-06-09 05:21:36 +00:00
Logan McNaughton fce57f8ee2 move netplay poll (#1042) 2026-06-08 19:19:12 +00:00
Logan McNaughton d48423da77 cargo update (#1040)
* cargo update

* add cmake for mac

* more
2026-06-08 14:28:51 +00:00
Logan McNaughton f691578698 use idle (#1039) 2026-06-08 13:23:22 +00:00
Logan McNaughton 42a4a46dd9 Fix formatting for device information in bug report (#1038)
Updated formatting for CPU and GPU fields in bug report template.
2026-06-08 08:53:20 +00:00
Logan McNaughton e1d715bc38 state updates (#1036)
* state updates

* more
2026-06-08 08:40:58 +00:00
Logan McNaughton 4285290452 update issue template (#1037) 2026-06-08 07:32:20 +00:00
Logan McNaughton d79834eb50 add to rdp state (#1035)
* add to rdp state

* more

* more
2026-06-07 20:31:26 +00:00
Logan McNaughton 1244706b6e update issue template (#1034) 2026-06-07 18:43:58 +00:00
Logan McNaughton a1a7699516 use check_framebuffers (#1031) 2026-06-07 15:10:49 +00:00
Logan McNaughton 577741907f move speed limiter out of save state (#1030) 2026-06-07 09:55:18 +00:00
Logan McNaughton ba35935d2e Update issue templates (#1029) 2026-06-07 08:44:52 +00:00
Logan McNaughton a7df8d837f don't count FPS during rollback (#1027) 2026-06-07 07:28:09 +00:00
Logan McNaughton 820c577f5f use tokio::spawn (#1026) 2026-06-07 06:21:16 +00:00
Logan McNaughton 8b3a1a40f2 improve tokio setup (#1024)
* improve tokio setup

* more

* more

* Revert "more"

This reverts commit e0e1db4482.

* more

* more
2026-06-06 20:44:30 +00:00
Logan McNaughton 21238c8e41 cleanup if statement (#1023) 2026-06-06 19:20:53 +00:00
Logan McNaughton 73f145a35c Increase ping sample size from 10 to 12 (#1022) 2026-06-06 13:57:53 +00:00
Logan McNaughton 3184305c41 increase disconnect timeout (#1020) 2026-06-06 12:42:13 +00:00
Logan McNaughton 06dc40fd3e change netplay auth (#1019) 2026-06-06 12:22:23 +00:00
Logan McNaughton c7ac333cb7 fix ping calc (#1018) 2026-06-06 09:30:36 +00:00
Logan McNaughton 1d6e415422 use wss (#1017)
* use wss

* more
2026-06-06 09:06:40 +00:00
Logan McNaughton c89c5d1cc3 increase disconnect timeout (#1016)
* increase disconnect timeout

* more
2026-06-06 06:22:52 +00:00
Logan McNaughton 19dc966abd use different rooms for lobby and game (#1009) 2026-06-05 21:35:59 +00:00
Logan McNaughton 077c1f439c is_system_pal (#1007) 2026-06-05 17:21:29 +00:00
Logan McNaughton 9140138843 Don't render frame during roll forward (#1006)
* no update check on android

* more

* more

* more

* more

* more
2026-06-05 15:37:56 +00:00
Logan McNaughton 618e3a32f2 netplay error checking (#1005)
* netplay error checking

* more

* more

* more

* more

* more

* more

* more

* more
2026-06-05 12:16:01 +00:00
Logan McNaughton 681f8670f5 GGRS netplay (#1004)
* update netplay

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-06-05 07:43:31 +00:00
Logan McNaughton 0a44124c20 process savestates on swap buffer (#1003) 2026-06-01 20:30:31 +00:00
Logan McNaughton eb64a9ad43 switch idle command (#1002)
* switch idle command

* more
2026-06-01 11:43:39 +00:00
Logan McNaughton 5a5ff40efc set_rdram should not be needed (#1001)
* set_rdram should not be needed

* more
2026-06-01 09:07:37 +00:00
Connor Welsh cd6726d451 set application ID (#998) 2026-05-31 21:05:26 +00:00
Logan McNaughton cbc973a559 idle RDP before load state (#1000)
* idle RDP before load state

* more
2026-05-31 20:25:28 +00:00
Logan McNaughton d968983ceb Savestate tweaks (#999)
* move savestate processing

* fix stack overflow

* more

* more
2026-05-31 19:29:28 +00:00
Logan McNaughton 5175d50aa8 save video state before cloning device (#997) 2026-05-31 17:49:07 +00:00
Logan McNaughton e25818b451 keep static device intact (#996) 2026-05-31 16:22:50 +00:00
Logan McNaughton 4453e66bcf Improve state load time (#995)
* load state speed improvements

* more

* more

* more
2026-05-31 14:40:40 +00:00
Logan McNaughton 19432f33ba use hashmap for lut (#994) 2026-05-31 07:45:02 +00:00
Logan McNaughton 5c7170268e Don't store luts in savestate (#993)
* use hashmap for tlb

* more

* more

* more

* more

* more
2026-05-31 06:37:11 +00:00
Logan McNaughton 79d2c023e6 change message length (#992) 2026-05-30 19:16:15 +00:00
Logan McNaughton 864175fae6 Rewind (#991)
* rewind

* more
2026-05-30 19:00:33 +00:00
Logan McNaughton 7fc2412c9c move state to new thread (#990)
* move state to new thread

* more

* more
2026-05-30 12:26:38 +00:00
Logan McNaughton 3fdbe9a4f7 implement default for Ui (#989)
* implement default for Ui

* more
2026-05-30 11:36:32 +00:00
Logan McNaughton 399b5d65e5 Fix MBC3 latch (#987)
* Fix MBC3 latch

* more
2026-05-30 07:43:22 +00:00
Logan McNaughton 24e5a3fac4 move joystick update (#985) 2026-05-29 14:51:27 +00:00
Logan McNaughton 31420b6777 move state compression into thread (#984) 2026-05-29 13:01:45 +00:00
Logan McNaughton 7379484932 add load state func for audio (#983)
* add load state func for audio

* more

* more
2026-05-29 08:06:46 +00:00
Logan McNaughton d36ce10ab3 remove nested listview for cheats (#982) 2026-05-29 05:57:57 +00:00
Logan McNaughton 57807f09c8 update NDK (#981)
* update NDK

* Add ANDROID_NDK_ROOT environment variable
2026-05-29 05:42:41 +00:00
Logan McNaughton 0745d75c2f Add Android APK download link to README (#980) 2026-05-28 20:29:10 +00:00
Logan McNaughton a78f47ecfe fix download folder (#979) 2026-05-28 19:53:59 +00:00
Logan McNaughton 1df705c4f9 fix keystore path (#978) 2026-05-28 19:17:07 +00:00
Logan McNaughton f7ed2c5273 move itch upload (#977) 2026-05-28 18:33:11 +00:00
Logan McNaughton 8bd4c936f8 bump rust (#976) 2026-05-28 18:09:08 +00:00
Logan McNaughton 34e55d9cb5 fix small speed limiter bug (#975)
* fix small speed limiter bug

* more
2026-05-28 13:54:42 +00:00
Logan McNaughton 8a8596a66b clippy fixes (#974) 2026-05-28 13:25:23 +00:00
Logan McNaughton 40ab4ceb2b fix name of apk (#973) 2026-05-28 13:10:39 +00:00
Logan McNaughton a8dbbba6e3 update sessions async (#972)
* update sessions async

* more
2026-05-28 12:58:26 +00:00
Logan McNaughton 5b0cc0130e minSdk to 33 for android (#971) 2026-05-28 11:24:49 +00:00
Logan McNaughton bb32a0a8ba update android release settings (#970)
* update android release settings

* more

* more

* more

* more
2026-05-28 11:04:52 +00:00
Logan McNaughton e23894c2e6 make usb handle same as discord (#969) 2026-05-28 10:04:44 +00:00
Logan McNaughton e0efa80f44 Don't reload RDP processor (#968)
* change rdram pointer

* more

* more

* more

* more

* more

* more

* more
2026-05-28 09:56:35 +00:00
Logan McNaughton cb734f8df1 fix overflow in load state (#967)
* fix overflow in load state

* more
2026-05-28 09:21:58 +00:00
Logan McNaughton 3197d415e0 timeout handles (#966)
* timeout handles

* more
2026-05-28 09:12:40 +00:00
Logan McNaughton 9dfc42c723 use setSustainedPerformanceMode (#965)
* use setSustainedPerformanceMode

* more
2026-05-27 18:45:43 +00:00
Logan McNaughton 14b973ec5f remove event watch on close (#964) 2026-05-27 16:24:25 +00:00
Logan McNaughton 81a3cea9c7 fix ra init on android (#963)
* fix ra init on android

* more

* more
2026-05-27 13:39:37 +00:00
Logan McNaughton dc6138076f use const for unknown name (#962) 2026-05-27 12:23:43 +00:00
Logan McNaughton a9f7285c0f auto set version (#961) 2026-05-27 12:11:02 +00:00
Logan McNaughton 7106a461b5 Update android manifest (#960)
* Update android manifest

* more
2026-05-27 11:17:27 +00:00
Logan McNaughton 69e3ebcc26 match controllers on android (#959)
* match controllers on android

* more

* more
2026-05-27 10:03:36 +00:00
Logan McNaughton f80f0f5f02 handle pause on android (#958) 2026-05-27 08:11:48 +00:00
Logan McNaughton a447bc8104 pass all args (#957) 2026-05-26 20:43:21 +00:00
Logan McNaughton 32f54f157b Run rom android (#956)
* run rom android

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-26 18:09:57 +00:00
Logan McNaughton 9c9f1a3000 connect to native code (#955)
* connect to native code

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-26 13:03:00 +00:00
Logan McNaughton 9d7b13d4c9 include sdl libs for android (#954) 2026-05-26 07:39:51 +00:00
Logan McNaughton 60cbf59ddf add intent for input profile (#952)
* add intent for input profile

* more

* more
2026-05-25 19:35:04 +00:00
Logan McNaughton 6f00e630e0 fix order in activity result (#951)
* fix order in activity result

* more
2026-05-25 18:37:12 +00:00
Logan McNaughton 7599f71c17 add sdl for android 2026-05-25 19:59:24 +02:00
Logan McNaughton 411cb0efa7 remove dialog windows (#948) 2026-05-25 09:31:33 +00:00
Logan McNaughton 00ec554783 wait a little longer for broadcast response (#947) 2026-05-23 18:57:44 +00:00
Logan McNaughton d25d3a460a save settings on android (#946)
* save settings on android

* more

* more
2026-05-23 13:44:14 +00:00
Logan McNaughton 8eec90c35c remove focusscope (#945) 2026-05-23 09:36:33 +00:00
Logan McNaughton a596e01ef2 clean up controller detection (#943) 2026-05-23 08:19:44 +00:00
Logan McNaughton 43ebc37f87 fix RA on android (#942) 2026-05-23 07:31:39 +00:00
Logan McNaughton 6223f10fae improve jni paths (#941) 2026-05-23 07:04:23 +00:00
Logan McNaughton c16e927288 use native-tls (#940)
* use native-tls

* Add linker argument for aarch64 macOS target

* Update linker settings in config.toml

Remove linker configuration for macOS and consolidate for Windows.

* Revert "Update linker settings in config.toml"

This reverts commit 33c8da07bf.

* more
2026-05-23 05:34:01 +00:00
Logan McNaughton 6ef33204e8 remove duplicate code (#938) 2026-05-22 17:49:15 +00:00
Logan McNaughton 3f9d62248a rom reading on android (#937) 2026-05-22 14:42:08 +00:00
Logan McNaughton 18fbd8e56c Remove AndroidX usage declaration (#936)
Removed AndroidX usage declaration from properties.
2026-05-22 13:50:50 +00:00
Logan McNaughton e5c74d05aa Improve some dialogs (#935)
* improve dialogs

* more
2026-05-22 10:43:37 +00:00
Logan McNaughton 82c22dc18e check file exists on android (#934)
* check file exists on android

* more

* more

* more
2026-05-22 07:43:20 +00:00
Logan McNaughton 66a74b5693 keep file permissions (#933) 2026-05-22 06:05:59 +00:00
Logan McNaughton 367b8bdb5c Handle intent results (#932)
* Handle intent results

* more

* more

* more

* more
2026-05-21 20:06:55 +00:00
Logan McNaughton b865c88d99 Use Gradle (#931)
* remove cargo-apk

* add gradle

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* Revert "more"

This reverts commit 19395aac13.

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-21 16:16:12 +00:00
Logan McNaughton 53b39010a0 no vru on android (#930)
* no vru on android

* more

* more
2026-05-21 05:57:09 +00:00
Logan McNaughton 7ad8bed7df Netplay window adjustments (#929)
* adjust netplay windows

* more

* more
2026-05-20 15:01:31 +00:00
Logan McNaughton 01ea4c422a hold a ref to AndroidApp (#928)
* hold a ref to AndroidApp

* more

* more

* more

* more

* more
2026-05-20 14:16:17 +00:00
Logan McNaughton 7eb090a622 fix some popups (#927) 2026-05-20 10:43:24 +00:00
Logan McNaughton 48956dc016 move some android guards around (#926)
* move some android guards around

* more
2026-05-20 09:41:12 +00:00
Logan McNaughton 2eb8a0dc1d move save_settings into callback (#925) 2026-05-20 08:34:33 +00:00
Logan McNaughton c53ffebece only show window in safe area (#924) 2026-05-20 07:47:34 +00:00
Logan McNaughton 06d0aa53bc enable ui dragging (#923) 2026-05-20 07:14:43 +00:00
Logan McNaughton 28b1424357 don't hold android app reference (#922)
* don't hold android app reference

* more

* more
2026-05-20 06:57:56 +00:00
Logan McNaughton e852439b6a Fix open on Android (#921)
* fix open

* more

* more

* mroe
2026-05-19 19:56:36 +00:00
Logan McNaughton 5850c514f2 Populate controller list on android (#920)
* list controllers in UI for android

* more

* more
2026-05-19 17:30:09 +00:00
Logan McNaughton 11a49fd687 open crate only for gui (#919) 2026-05-19 13:36:48 +00:00
Logan McNaughton be9ba1366e move some android guards (#917)
* move some android guards

* more
2026-05-19 12:10:25 +00:00
Logan McNaughton dbe67f1034 More UI tweaks (#916)
* More UI tweaks

* more

* more
2026-05-19 09:50:01 +00:00
Logan McNaughton 7eecfa3275 use robius open (#915)
* use robius open

* more
2026-05-19 08:50:42 +00:00
Logan McNaughton e1b4039a52 dont use open on android (#914) 2026-05-19 07:43:55 +00:00
Logan McNaughton 9f2cfb5a9f read RA settings from CLI (#913)
* read RA settings from CLI

* more
2026-05-19 07:20:08 +00:00
Logan McNaughton 41e66825d0 Update UI (#911)
* rework UI

* more

* more

* more
2026-05-19 06:50:00 +00:00
Logan McNaughton a78393ca91 More basic Android stuff (#910)
* enable tokio

* data dirs

* jni pointer

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-18 15:08:01 +00:00
Logan McNaughton 8dde32cb21 fix label (#909) 2026-05-17 19:53:10 +00:00
Logan McNaughton 1538513fb7 add apk icon (#908)
* add apk icon

* more

* more

* more

* more
2026-05-17 18:07:57 +00:00
Logan McNaughton 30c8aef24e move netplay_id (#907) 2026-05-17 16:44:21 +00:00
Logan McNaughton 8daa65d93e only use --release for tags (#906)
* only use --release for tags

* more

* more

* more
2026-05-17 16:14:48 +00:00
Logan McNaughton df5fcae69b Remove Android SDK setup from build.yml (#905) 2026-05-17 15:26:13 +00:00
Logan McNaughton e3b63908de show controller during config (#904) 2026-05-17 14:43:37 +00:00
Logan McNaughton 8c53de9cc7 Don't set NDK version (#903)
* Set ANDROID_NDK_ROOT

* Update build.yml
2026-05-17 14:11:29 +00:00
Logan McNaughton 78cb758a26 Add LIBCLANG_PATH to build workflow (#902) 2026-05-17 13:40:14 +00:00
Logan McNaughton 8fea9ef212 update ndk (#901) 2026-05-17 09:51:50 +00:00
Logan McNaughton 38fdc24a49 Use cargo-ndk for env (#900)
* Use cargo-ndk for env

* Update build.yml
2026-05-17 09:00:57 +00:00
Logan McNaughton 069c64ab08 build apk (#899)
* build apk

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-16 20:09:19 +00:00
Logan McNaughton 2005fb1421 don't update window when minimized (#898) 2026-05-16 18:46:00 +00:00
Logan McNaughton 04b15fee2f fail early if cheats cannot be read (#897) 2026-05-15 18:05:20 +00:00
Logan McNaughton eda3dda613 error if cheats cannot be read (#896) 2026-05-15 17:44:04 +00:00
Logan McNaughton 03be98b571 build android lib (#895)
* build android lib

* more

* more

* more

* more
2026-05-15 14:10:23 +00:00
Logan McNaughton 93bf103f03 move around RA code (#894)
* move around RA code

* more

* more

* more
2026-05-15 12:36:11 +00:00
Logan McNaughton c8f485cd93 Build clib (#893)
* Build clib

* more

* more

* more

* linker

* more

* more

* more

* more

* more

* more
2026-05-15 11:18:48 +00:00
Logan McNaughton 3a301c517d shorten message (#891) 2026-05-15 06:58:19 +00:00
Logan McNaughton d17c577ccd use port 48646 for UNFloader (#890) 2026-05-14 19:55:42 +00:00
Logan McNaughton 8c331c25eb Sort translation files (#889)
* Sort translation files

* more
2026-05-14 15:16:53 +00:00
Logan McNaughton 8341ef5731 Add Discord Rich Presence (#888)
* add rich presence

* more

* more

* more

* more

* more

* more

* more

* more

* more

* update translations

* more
2026-05-14 14:30:36 +00:00
Logan McNaughton 40a29d927f shorten some messages (#887) 2026-05-14 08:34:42 +00:00
Logan McNaughton 9846c2243c fix typo in RAM name (#885) 2026-05-13 12:48:18 +00:00
Logan McNaughton c1dc4fe589 automatically attach assets to release (#884)
* automatically attach assets to release

* more
2026-05-13 08:03:15 +00:00
Logan McNaughton 06052b729b add missing newline (#881) 2026-05-12 11:55:42 +00:00
Logan McNaughton d7c912b53e Update russian translation (#880) 2026-05-12 11:16:28 +00:00
Logan McNaughton b792ebbd45 Change German translation for 'Transfer Pak' (#879) 2026-05-12 10:26:41 +00:00
Logan McNaughton e7071047e3 Add Russian translation (#878) 2026-05-12 10:22:44 +00:00
Logan McNaughton 6496288c4e don't reallocate rdram during load state (#877)
* don't reallocate rdram during load state

* more
2026-05-12 08:11:46 +00:00
Logan McNaughton 1ab8987f2d Update lint.yml to include fetch-depth option (#876)
Add fetch-depth option to checkout step in lint workflow.
2026-05-12 06:55:01 +00:00
Logan McNaughton 034b39bec5 Add Brazilian Portuguese translation (#875)
* Add Brazilian Portuguese translation"

* fix typo

* couple more fixes
2026-05-12 06:48:33 +00:00
Logan McNaughton 5368ae3d37 German translation updates (#874) 2026-05-12 05:46:04 +00:00
Logan McNaughton f963742009 put back checks for proper type of controller (#873) 2026-05-11 15:42:15 +00:00
Logan McNaughton a8ae9dbc6a Add more starter translations (#872)
* Add more starter translations

* more
2026-05-11 11:41:45 +00:00
Logan McNaughton b55a031971 Update translation readme (#871) 2026-05-11 10:26:40 +00:00
Logan McNaughton 27ffedbdee es changes (#870) 2026-05-11 05:35:22 +00:00
Logan McNaughton 493f08afaf remove some spaces from translation strings (#868) 2026-05-10 18:44:18 +00:00
Logan McNaughton 0659da24fa bundle AI generated spanish translation (#867)
* bundle AI generated spanish translation

* more

* more

* more

* more

* more

* more

* more
2026-05-10 14:24:34 +00:00
Logan McNaughton 5596dad111 improve some translation wrappers (#866) 2026-05-10 13:08:13 +00:00
Logan McNaughton a5b7561c51 clean up read_cart_word (#865)
* clean up read_cart_word

* more

* more
2026-05-10 09:28:48 +00:00
Logan McNaughton fe23e4bad9 put default profile first (#864) 2026-05-09 18:58:29 +00:00
Logan McNaughton 5b3b8265e5 set offset using let match (#863) 2026-05-09 18:02:18 +00:00
Logan McNaughton a7cbf866e8 pump events before flush (#862) 2026-05-09 17:05:38 +00:00
Logan McNaughton 7fae21d7ac Use SDL_GetJoystickAxisInitialState (#858)
* add initial_state to input profile

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-09 13:47:55 +00:00
Logan McNaughton 55bf63210a Don't save timestamp to GB save (#860)
* use real time for GB RTC

* more

* more

* more
2026-05-09 13:12:20 +00:00
Logan McNaughton 2b72c24dbe GB RTC saving (#857)
* GB RTC saving

* GB RTC saving

* GB RTC saving

* GB RTC saving

* GB RTC saving

* GB RTC saving

* GB RTC saving

* GB RTC saving

* GB RTC saving

* more
2026-05-09 07:57:25 +00:00
Logan McNaughton 455365a71d move sdl hint (#856) 2026-05-09 06:35:00 +00:00
Logan McNaughton 1f6b575440 move gb time into variable (#855)
* move gb time into variable

* more
2026-05-08 20:05:25 +00:00
Logan McNaughton 86304018dd remove un-needed window() (#854)
* remove un-needed window()

* more
2026-05-08 14:36:23 +00:00
Logan McNaughton c44ab9602c GBC RTC support (#853)
* GBC RTC support

* more

* more

* more

* more

* more

* more
2026-05-08 14:03:13 +00:00
Logan McNaughton 13be114823 look for bin in compressed files (#851)
* look for bin in compressed files

* more

* more

* more

* more

* more

* more

* more
2026-05-08 07:36:16 +00:00
Logan McNaughton f793eb06be bump to 1.1.20 (#849) 2026-05-07 06:49:49 +00:00
Logan McNaughton 61532f7b45 move sdl init (#848)
* move sdl init

* more

* more

* more

* more
2026-05-06 20:09:21 +00:00
Logan McNaughton a69a74bf37 cleanup some if statements (#846) 2026-05-06 11:57:04 +00:00
Logan McNaughton 6d4d5468f9 save rom dir (#845) 2026-05-06 11:17:28 +00:00
Logan McNaughton 7015d44da7 fix ghost image with SSAA (#844) 2026-05-06 11:05:48 +00:00
Logan McNaughton 172867527b add option for SSAA (#843) 2026-05-06 09:52:13 +00:00
Logan McNaughton 323e8cf1f0 move sdl quit calls (#842)
* move sdl quit calls

* more
2026-05-06 08:59:05 +00:00
Logan McNaughton 172d10d7cb update controllers when clicking on controller config (#841)
* update controllers when clicking on controller config

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-05-06 07:44:39 +00:00
Logan McNaughton 945d456f9c disable fps display on close (#839)
* disable fps display on close

* another check
2026-05-04 12:52:25 +00:00
Logan McNaughton a4f635265b guards around postcard (#838) 2026-05-04 11:52:46 +00:00
Logan McNaughton f45f951af8 fast forward controller hotkey (#837)
* fast forward controller hotkey

* more
2026-05-04 08:59:20 +00:00
Logan McNaughton 883852a828 more error messages (#836) 2026-05-04 08:36:50 +00:00
Logan McNaughton 90052257bb Use eprintln (#835)
* use eprintln

* more

* more
2026-05-04 07:49:38 +00:00
Logan McNaughton 1184d3d77a better error message (#834) 2026-05-03 14:22:50 +00:00
Logan McNaughton 12280bb35b more crash protection (#832)
* more crash protection

* more

* more
2026-05-03 14:02:17 +00:00
Logan McNaughton 17f3f2dc63 more crash protection (#833)
* more crash protection

* more
2026-05-03 13:59:59 +00:00
Logan McNaughton 3d6ca17a9a more crash protection (#831) 2026-05-03 13:11:08 +00:00
Logan McNaughton 01863a70a8 avoid some controller crashes (#830)
* avoid some controller crashes

* more

* more
2026-05-03 07:50:13 +00:00
Logan McNaughton 801cc1bc74 cargo update (#827) 2026-05-03 05:49:27 +00:00
Logan McNaughton 9ed8d835c0 add merge_group to workflow (#828) 2026-05-03 05:49:07 +00:00
Logan McNaughton b754891d45 ensure audio is playing (#826)
* ensure audio is playing

* more
2026-05-03 07:18:28 +02:00
Logan McNaughton 9cc6d48493 ensure file dialog order for transferpak (#824)
* ensure file dialog order for transferpak

* more
2026-05-02 22:13:59 +02:00
Logan McNaughton b865538d6e avoid panic with invalid cheat (#823)
* avoid panic with invalid cheat

* more

* more
2026-05-02 17:59:51 +02:00
Logan McNaughton abacaddf8a avoid crash on missing GB files (#822)
* avoid crash on missing GB files

* more

* more
2026-05-02 17:06:56 +02:00
Logan McNaughton b86c5daa2c add controller hotkeys (#821)
* add controller hotkeys

* more

* more

* more

* more

* more

* more
2026-05-02 09:04:42 +02:00
Logan McNaughton df8d7d9ff0 allow single controller mpk files (#820) 2026-05-01 21:32:08 +02:00
Logan McNaughton 8c7d3d4893 protect against null ptr dereference (#819)
* protect against null ptr dereference

* more
2026-05-01 20:44:14 +02:00
Logan McNaughton b88f041bd8 don't check for update inside itch app (#817) 2026-05-01 17:42:07 +02:00
Logan McNaughton e860b96bd8 Controller code cleanup (#816)
* controller hotkeys

* more

* more

* more

* more

* more
2026-04-28 08:37:59 +02:00
Logan McNaughton b2ef544513 handle controller reconnection (#815) 2026-04-27 16:38:20 +02:00
dependabot[bot] ca00ee1a42 Bump apple-actions/import-codesign-certs from 6 to 7 (#814)
Bumps [apple-actions/import-codesign-certs](https://github.com/apple-actions/import-codesign-certs) from 6 to 7.
- [Release notes](https://github.com/apple-actions/import-codesign-certs/releases)
- [Commits](https://github.com/apple-actions/import-codesign-certs/compare/v6...v7)

---
updated-dependencies:
- dependency-name: apple-actions/import-codesign-certs
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 05:59:33 +02:00
Logan McNaughton 511d8461c2 add note about modifying profile (#812) 2026-04-26 17:46:12 +02:00
Logan McNaughton fe42efadfa limit frequent audio adjustments (#810) 2026-04-25 17:05:54 +02:00
Logan McNaughton c1e73320b1 adjust audio buffer management (#809)
* adjust audio buffer management

* more

* more
2026-04-25 16:05:28 +02:00
Logan McNaughton 47dd6d61b0 update setup-butler (#808) 2026-04-25 11:20:56 +02:00
Logan McNaughton 212557610b use bash shell (#807) 2026-04-25 07:53:30 +02:00
Logan McNaughton 5e6a7731f4 update to 1.1.19 (#806)
* update to 1.1.19

* more
2026-04-25 07:22:58 +02:00
Logan McNaughton 65c63dfc5d Always set user agent (#805)
* Always set user agent

* more
2026-04-25 07:15:47 +02:00
Logan McNaughton ae4e7cb01f Update download links (#804) 2026-04-24 22:12:24 +02:00
Logan McNaughton a233c72dad add itch.io link (#803) 2026-04-24 10:54:47 +02:00
Logan McNaughton b267b83248 upload to itch.io (#802) 2026-04-24 10:23:28 +02:00
Logan McNaughton 6c2ae4dc5a update mac signing command (#801) 2026-04-23 20:01:01 +02:00
Logan McNaughton 0a884e85df Fix macos sandboxing (#800)
* Fix macos sandboxing

* more

* more

* more

* more

* more

* more

* more
2026-04-23 19:32:53 +02:00
Logan McNaughton 47098f60dd use timer for message lengths (#799)
* use timer for message lengths

* more

* more

* more

* more

* more

* more
2026-04-23 18:00:15 +02:00
125 changed files with 38966 additions and 6382 deletions
+5 -1
View File
@@ -11,7 +11,10 @@ rustflags = ["-C", "link-arg=/STACK:8388608"]
rustflags = ["-C", "target-cpu=x86-64-v3"]
[target.'cfg(all(target_arch = "aarch64", target_os = "macos"))']
rustflags = ["-C", "target-cpu=apple-m1"]
rustflags = [
"-C", "target-cpu=apple-m1",
"-C", "link-arg=-headerpad_max_install_names"
]
[target.'cfg(all(target_arch = "aarch64", not(target_os = "macos")))']
rustflags = ["-C", "target-cpu=cortex-a76"]
@@ -21,3 +24,4 @@ CC = "clang"
CXX = "clang++"
AR = "llvm-ar"
SDKROOT = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
CMAKE_POLICY_VERSION_MINIMUM = "3.5" # for freetype
+33
View File
@@ -0,0 +1,33 @@
---
name: Bug report
about: Bugs only! No feature requests
title: ''
labels: ''
assignees: ''
type: Bug
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Device (please complete the following information):**
- OS: [e.g. Windows 11]
- CPU: [e.g. Intel i7-1365U]
- GPU: [e.g. Nvidia 5060 Ti]
**Additional context**
Add any other context about the problem here.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+118 -35
View File
@@ -4,15 +4,19 @@ permissions:
on:
push:
branches: [ "main" ]
branches: ["main"]
tags:
- "v*"
pull_request:
branches: [ "main" ]
branches: ["main"]
merge_group:
env:
CARGO_TERM_COLOR: always
RA_HARDCORE: ${{ github.ref_type == 'tag' && 'true' || 'false' }}
NETPLAY_ID: ${{ github.ref_type == 'tag' && secrets.NETPLAY_ID || 'dev' }}
BUILD_PROFILE: ${{ github.ref_type == 'tag' && 'release' || 'dev' }}
BUILD_DIR: ${{ github.ref_type == 'tag' && 'release' || 'debug' }}
jobs:
build-linux:
@@ -30,7 +34,7 @@ jobs:
arch: linux-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
@@ -40,7 +44,8 @@ jobs:
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
run:
echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
-d':' -f2 | xargs)" >> $GITHUB_ENV
- name: Install SDL Dependencies
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
@@ -53,23 +58,21 @@ jobs:
libxtst-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev
libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev
libudev-dev libthai-dev libpipewire-0.3-dev libwayland-dev
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
libdecor-0-dev liburing-dev libfontconfig-dev
version: ${{ matrix.target }}-1.0
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
force-url:
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-${{
matrix.llvm }}.tar.xz
- name: Build
run: |
cargo build --no-default-features -v --target=${{ matrix.target }}
cargo build --release -v --target=${{ matrix.target }}
cargo build --profile ${{ env.BUILD_PROFILE }} -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
env:
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64 output/gopher64-${{ matrix.arch }}
- name: Upload file
uses: actions/upload-artifact@v7
with:
@@ -92,7 +95,7 @@ jobs:
arch: windows-aarch64
llvm: woa64
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
@@ -109,19 +112,18 @@ jobs:
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
force-url:
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-${{ matrix.llvm
}}.exe
- name: Build
run: |
cargo build --no-default-features -v --target=${{ matrix.target }}
cargo build --release -v --target=${{ matrix.target }}
cargo build --profile ${{ env.BUILD_PROFILE }} -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
env:
CC: clang-cl
CXX: clang-cl
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
- name: Upload unsigned artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v7
@@ -161,7 +163,7 @@ jobs:
arch: macos-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
@@ -171,48 +173,129 @@ jobs:
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
run:
echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
-d':' -f2 | xargs)" >> $GITHUB_ENV
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
force-url:
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-macOS-${{
matrix.llvm }}.tar.xz
- name: Build
run: |
cargo install cargo-bundle
brew install molten-vk
cargo build --no-default-features -v --target=${{ matrix.target }}
cargo bundle --release --target=${{ matrix.target }} --format osx
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64
install_name_tool -change /opt/homebrew/opt/libpng/lib/libpng16.16.dylib \
@executable_path/../Frameworks/libpng16.16.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
cargo build --profile ${{ env.BUILD_PROFILE }} --no-default-features -v --target=${{ matrix.target }}
mv target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64 target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64-cli
cargo bundle --profile ${{ env.BUILD_PROFILE }} --target=${{ matrix.target }} --format osx
cp target/${{ matrix.target }}/${{ env.BUILD_DIR }}/gopher64-cli target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
env:
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
MACOSX_DEPLOYMENT_TARGET: "15.7"
MACOSX_DEPLOYMENT_TARGET: "15.0"
- name: Sign Bundle Dev
if: ${{ github.ref_type != 'tag' }}
run: |
codesign -f --entitlements data/macos/entitlements_dev.plist --deep --timestamp --options runtime -s - target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
codesign -f --entitlements data/macos/entitlements_dev.plist --deep --timestamp --options runtime -s - target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
- name: Import Codesign Certs
if: ${{ github.ref_type == 'tag' }}
uses: apple-actions/import-codesign-certs@v6
uses: apple-actions/import-codesign-certs@v7
with:
p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_FILE_BASE64 }}
p12-password: ${{ secrets.APPLE_CERTIFICATES_PASSWORD }}
- name: Sign Bundle Release
if: ${{ github.ref_type == 'tag' }}
run: |
cp data/macos/embedded.provisionprofile target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/embedded.provisionprofile
codesign -f --entitlements data/macos/entitlements_prod.plist --deep --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
zip -r gopher64-${{ matrix.arch }}.zip target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app/Contents/Frameworks/libMoltenVK.dylib
codesign -f --entitlements data/macos/entitlements_child.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
codesign -f --entitlements data/macos/entitlements_prod.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
zip -r gopher64-${{ matrix.arch }}.zip target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
xcrun notarytool submit --wait --apple-id "${{ secrets.APPLE_ID }}" --team-id "${{ secrets.APPLE_DEVELOPER_ID }}" --password "${{ secrets.APPLE_APP_PASSWORD }}" gopher64-${{ matrix.arch }}.zip
xcrun stapler staple target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
xcrun stapler staple target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx/Gopher64.app
- name: Upload file
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: target/${{ matrix.target }}/release/bundle/osx
path: target/${{ matrix.target }}/${{ env.BUILD_DIR }}/bundle/osx
build-android:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
platform: 33
ndk: 29.0.14206865
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: x86_64-linux-android,aarch64-linux-android
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Setup Android SDK
uses: android-actions/setup-android@v4
with:
packages: "platforms;android-${{ matrix.platform }} ndk;${{ matrix.ndk }}"
- name: Decode keystore and create jks
if: ${{ github.ref_type == 'tag' }}
working-directory: android-project
run: |
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > app/keystore.jks
cat <<EOF > keystore.properties
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
keyAlias=gopher64
storeFile=keystore.jks
EOF
- name: Build
working-directory: android-project
run: |
cargo install cargo-ndk
./gradlew assemble${{ env.BUILD_PROFILE == 'release' && 'Release' || 'Debug' }}
mkdir output
cp app/build/outputs/apk/${{ env.BUILD_DIR }}/app-${{ env.BUILD_DIR }}.apk output/gopher64-android.apk
- name: Upload file
uses: actions/upload-artifact@v7
with:
name: gopher64-android
path: android-project/output/gopher64-android.apk
archive: false
collect-artifacts:
name: Collect release artifacts
needs: [build-linux, build-windows, build-macos, build-android]
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all workflow artifacts
uses: actions/download-artifact@v8
with:
skip-decompress: true
merge-multiple: true
path: release-artifacts
- name: Add files to release
uses: softprops/action-gh-release@v3
with:
draft: true
files: release-artifacts/*
- name: Setup butler
uses: remarkablegames/setup-butler@v3
- name: Upload to itch.io
run: |
butler push release-artifacts/gopher64-linux-x86_64 loganmc10/gopher64:linux-x86_64 --userversion ${GITHUB_REF_NAME#v}
butler push release-artifacts/gopher64-linux-aarch64 loganmc10/gopher64:linux-aarch64 --userversion ${GITHUB_REF_NAME#v}
butler push release-artifacts/gopher64-windows-x86_64.exe loganmc10/gopher64:windows-x86_64 --userversion ${GITHUB_REF_NAME#v}
butler push release-artifacts/gopher64-windows-aarch64.exe loganmc10/gopher64:windows-aarch64 --userversion ${GITHUB_REF_NAME#v}
butler push release-artifacts/gopher64-macos-aarch64.zip loganmc10/gopher64:macos-aarch64 --userversion ${GITHUB_REF_NAME#v}
butler push release-artifacts/gopher64-android.apk loganmc10/gopher64:android --userversion ${GITHUB_REF_NAME#v}
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
+16 -6
View File
@@ -2,9 +2,10 @@ name: Rust-lint
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
merge_group:
permissions:
contents: read
@@ -16,14 +17,17 @@ jobs:
lint-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ""
components: clippy, rustfmt
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
run:
echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut
-d':' -f2 | xargs)" >> $GITHUB_ENV
- name: Install SDL Dependencies
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
@@ -36,17 +40,23 @@ jobs:
libxtst-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev
libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev
libudev-dev libthai-dev libpipewire-0.3-dev libwayland-dev
libdecor-0-dev liburing-dev libfreetype-dev libfontconfig-dev
libdecor-0-dev liburing-dev libfontconfig-dev gettext
version: lint-1.0
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
force-url:
https://github.com/llvm/llvm-project/releases/download/llvmorg-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-X64.tar.xz
- name: Rustfmt Check
uses: actions-rust-lang/rustfmt@v1
- name: Lint
run: |
cargo install slint-tr-extractor
cargo clippy -- -Dwarnings
cargo clippy --no-default-features -- -Dwarnings
find -name \*.slint | sort | xargs slint-tr-extractor -o /tmp/translations.pot
msgcmp --use-untranslated /tmp/translations.pot data/translations/gopher64.pot
msgcmp --use-untranslated data/translations/gopher64.pot /tmp/translations.pot
find data/translations -name "*.po" | xargs -I {} msgcmp {} data/translations/gopher64.pot
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
checks: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: rustsec/audit-check@v2
Generated
+2862 -1040
View File
File diff suppressed because it is too large Load Diff
+33 -19
View File
@@ -1,50 +1,65 @@
[package]
name = "gopher64"
version = "1.1.17"
version = "1.1.34"
edition = "2024"
rust-version = "1.95.0"
rust-version = "1.97.0"
[features]
default = ["gui"]
gui = ["dep:rfd", "dep:slint", "dep:slint-build"]
gui = ["dep:rfd", "dep:slint", "dep:open", "dep:slint-build"]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
dirs = "6.0"
zip = "8.0"
sevenz-rust2 = "0.20"
sevenz-rust2 = "0.21"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-skia", "accessibility"], optional = true }
open = "5.3"
sha2 = "0.11"
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source-static", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
rfd = {version = "0.17", optional = true }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs", "process"] }
tokio = {version = "1.46", features = ["rt-multi-thread", "fs", "process"] }
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
regex = "1.11"
tokio-tungstenite = "0.29"
tokio-tungstenite = { version = "0.29", features = ["native-tls-vendored"] }
matchbox_socket = "0.14"
ggrs = { version = "0.13", features = ["sync-send"] }
rustc-hash = "2.1"
futures = "0.3"
socket2 = { version = "0.6", features = ["all"] }
rand = "0.10"
rand = { version = "0.10", features = ["serde"] }
fatfs = "0.3"
semver = "1.0"
postcard = { version = "1.1", features = ["use-std"] }
clap = { version = "4.5", features = ["derive"] }
reqwest = { version = "0.13", features = ["json", "query"] }
reqwest = { version = "0.13", default-features = false, features = ["native-tls-vendored", "charset", "http2", "system-proxy", "json", "query"] }
discord-rich-presence = "1.1"
quick-xml = "0.41"
[target.'cfg(not(target_os = "android"))'.dependencies]
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-femtovg-wgpu", "accessibility"], optional = true }
rfd = {version = "0.17", optional = true }
dirs = "6.0"
open = { version = "5.3", optional = true }
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.6", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
[target.'cfg(target_os = "android")'.dependencies]
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-android-activity-06", "accessibility"], optional = true }
jni = "0.22"
sdl3-sys = { version = "0.6", features = ["build-from-source"] }
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
[build-dependencies]
winresource = "0.1"
bindgen = "0.72"
sdl3-src = "3.4"
slint-build = { version = "1.12", optional = true }
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
panic = "abort"
lto = "thin"
strip = true
@@ -52,12 +67,11 @@ strip = true
inherits = "release"
debug = true
strip = false
panic = "unwind"
[package.metadata.bundle]
name = "Gopher64"
identifier = "io.github.gopher64.gopher64"
category = "public.app-category.games"
icon = ["data/icon/gopher64_128x128.png","data/icon/gopher64_256x256.png","data/icon/gopher64_512x512.png"]
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib","/opt/homebrew/opt/freetype/lib/libfreetype.6.dylib","/opt/homebrew/opt/libpng/lib/libpng16.16.dylib"]
osx_minimum_system_version = "15.7"
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib"]
osx_minimum_system_version = "15.0"
+19 -5
View File
@@ -11,11 +11,21 @@ Gopher64 is a cross-platform N64 emulator. Some notable features:
## download
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
<a href="https://loganmc10.itch.io/gopher64"><img src="https://static.itch.io/images/badge.svg" width="200" ></a>
MacOS: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip
Windows:
* Standalone executable: [gopher64-windows-x86_64.exe](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe)
Linux: https://flathub.org/apps/io.github.gopher64.gopher64
MacOS:
* App Bundle: [gopher64-macos-aarch64.zip](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip)
* Homebrew: `brew install --cask gopher64`
Linux:
* Standalone executable: [gopher64-linux-x86_64](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-linux-x86_64)
* Flatpak: `flatpak install flathub io.github.gopher64.gopher64`
Android:
* APK: [gopher64-android.apk](https://github.com/gopher64/gopher64/releases/latest/download/gopher64-android.apk)
## wiki
@@ -31,7 +41,9 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
## netplay
Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the [server](https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
Gopher64 supports P2P netplay (online play with others).
Please read the [guide](https://github.com/gopher64/gopher64/wiki/Netplay-Guide) before trying out netplay.
## portable mode
@@ -56,7 +68,9 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
## contributing
I am very open to contributions! Please contact me via a GitHub issue or Discord (loganmc10) before doing substantial work on a PR.
PRs that are vibe coded and/or co-authored by AI agents (Plan/Agent mode) are not permitted. If you are going to submit a PR, it should be scoped to a single feature/improvement.
Please contact me via a GitHub issue or Discord (loganmc10) before doing substantial work on a PR.
## license
+39
View File
@@ -0,0 +1,39 @@
# Gradle files
.gradle/
build/
jniLibs/
# Local configuration file (sdk path, etc)
local.properties
keystore.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.aab
*.apk
output-metadata.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
app/src/main/java/org/libsdl/app/
+4
View File
@@ -0,0 +1,4 @@
set(ANDROID_PLATFORM $ENV{ANDROID_PLATFORM})
set(ANDROID_ABI $ENV{ANDROID_ABI})
set(ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES ON)
include("$ENV{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake")
+161
View File
@@ -0,0 +1,161 @@
import groovy.json.JsonSlurper
import java.util.Properties
import java.io.FileInputStream
plugins {
alias(libs.plugins.android.application)
}
kotlin {
jvmToolchain(17)
}
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "io.github.gopher64.gopher64"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
ndkVersion = "29.0.14206865"
defaultConfig {
applicationId = "io.github.gopher64.gopher64"
minSdk = 33
targetSdk = 36
versionCode = semverToVersionCode(cargoPackageVersion())
versionName = cargoPackageVersion()
ndk {
abiFilters.addAll(listOf("arm64-v8a", "x86_64"))
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
if (keystorePropertiesFile.exists()) {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
}
buildTypes {
release {
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs["release"]
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
packaging {
jniLibs {
excludes.add("lib/**/libsevenz_rust2*.so")
}
}
}
@Suppress("UNCHECKED_CAST")
fun cargoPackageVersion(packageName: String = "gopher64"): String {
val repoRoot = rootDir.parentFile
val jsonText = providers.exec {
workingDir(repoRoot)
commandLine("cargo", "metadata", "--format-version", "1", "--no-deps")
}.standardOutput.asText.get()
val parsedJson = JsonSlurper().parseText(jsonText) as Map<String, Any>
val packages = parsedJson["packages"] as List<Map<String, Any>>
return packages.first { it["name"] == packageName }["version"] as String
}
fun semverToVersionCode(version: String): Int {
val parts = version.substringBefore('-').split(".")
val (major, minor, patch) = Triple(parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
return major * 1_000_000 + minor * 1_000 + patch
}
val ndkBuild = tasks.register<Exec>("ndkBuild") {
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
workingDir = rootDir.parentFile
val toolchainPath = "$rootDir/android.toolchain.cmake"
environment("CMAKE_TOOLCHAIN_FILE", toolchainPath)
var minSdk = android.defaultConfig.minSdk
var ndkDir = androidComponents.sdkComponents.ndkDirectory.get().asFile.absolutePath
environment("ANDROID_NDK_HOME", "$ndkDir")
environment("ANDROID_NDK_ROOT", "$ndkDir")
environment("LIBCLANG_PATH", "$ndkDir/toolchains/llvm/prebuilt/linux-x86_64/musl/lib")
val jniType = if (isRelease) "release" else "debug"
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs"
commandLine(
"cargo", "ndk",
"--link-libcxx-shared",
"-P", "$minSdk",
"-t", "arm64-v8a",
"-t", "x86_64",
"-o", jniLibsFolder,
"build", "--lib",
"--profile", if (isRelease) "release" else "dev",
)
}
val sdlLibsArm64 = tasks.register<Copy>("sdlLibsArm64") {
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
val jniType = if (isRelease) "release" else "debug"
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs/arm64-v8a"
from("$rootDir/../target/aarch64-linux-android/$jniType")
into(jniLibsFolder)
include("libSDL*")
}
val sdlLibsX64 = tasks.register<Copy>("sdlLibsX64") {
val isRelease = gradle.startParameter.taskNames.any { it.endsWith("Release", ignoreCase = true) }
val jniType = if (isRelease) "release" else "debug"
val jniLibsFolder = "$rootDir/app/src/$jniType/jniLibs/x86_64"
from("$rootDir/../target/x86_64-linux-android/$jniType")
into(jniLibsFolder)
include("libSDL*")
}
tasks.named("preBuild") {
dependsOn(sdlLibsArm64)
dependsOn(sdlLibsX64)
}
tasks.named("sdlLibsArm64") {
dependsOn(ndkBuild)
}
tasks.named("sdlLibsX64") {
dependsOn(ndkBuild)
}
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
}
+82
View File
@@ -0,0 +1,82 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity {
java.lang.String nativeGetHint(java.lang.String); # Java-side doesn't use this, so it gets minified, but C-side still tries to register it
java.lang.String clipboardGetText();
boolean clipboardHasText();
void clipboardSetText(java.lang.String);
int createCustomCursor(int[], int, int, int, int);
void destroyCustomCursor(int);
android.app.Activity getContext();
boolean getManifestEnvironmentVariables();
android.view.Surface getNativeSurface();
void initTouch();
boolean isAndroidTV();
boolean isChromebook();
boolean isDeXMode();
boolean isTablet();
void manualBackButton();
int messageboxShowMessageBox(int, java.lang.String, java.lang.String, int[], int[], java.lang.String[], int[]);
void minimizeWindow();
boolean openURL(java.lang.String);
void requestPermission(java.lang.String, int);
boolean showToast(java.lang.String, int, int, int, int);
boolean sendMessage(int, int);
boolean setActivityTitle(java.lang.String);
boolean setCustomCursor(int);
void setOrientation(int, int, boolean, java.lang.String);
boolean setRelativeMouseEnabled(boolean);
boolean setSystemCursor(int);
void setWindowStyle(boolean);
boolean shouldMinimizeOnFocusLoss();
boolean showTextInput(int, int, int, int, int);
boolean supportsRelativeMouse();
int openFileDescriptor(java.lang.String, java.lang.String);
boolean showFileDialog(java.lang.String[], boolean, boolean, int);
java.lang.String getPreferredLocales();
java.lang.String formatLocale(java.util.Locale);
}
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager {
void closeDevice(int);
boolean initialize(boolean, boolean);
boolean openDevice(int);
boolean readReport(int, byte[], boolean);
int writeReport(int, byte[], boolean);
}
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLAudioManager {
void registerAudioDeviceCallback();
void unregisterAudioDeviceCallback();
void audioSetThreadPriority(boolean, int);
}
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLControllerManager {
void joystickSetSensorsEnabled(int, boolean);
void pollInputDevices();
void joystickSetLED(int, int, int, int);
void pollHapticDevices();
void hapticRun(int, float, int);
void hapticRumble(int, float, float, int);
void hapticStop(int);
}
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.vulkan.version" android:required="true"
android:version="0x00401000" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:icon="@mipmap/ic_launcher"
android:label="Gopher64"
android:appCategory="game"
android:hardwareAccelerated="true">
<activity android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:name=".SlintActivity" android:exported="true">
<meta-data android:name="android.app.lib_name" android:value="gopher64" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".N64Activity"
android:launchMode="singleTop"
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:preferMinimalPostProcessing="true"
android:exported="false">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,48 @@
package io.github.gopher64.gopher64
import android.content.Intent
import android.os.PowerManager
import android.content.Context
import android.os.Bundle
import android.util.Log
import org.libsdl.app.SDLActivity
class N64Activity : SDLActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val powerManager = getContext().getSystemService(Context.POWER_SERVICE) as PowerManager
if (powerManager.isSustainedPerformanceModeSupported) {
Log.v("SDL", "Enabling sustained performance mode")
window.setSustainedPerformanceMode(true)
} else {
Log.v("SDL", "Sustained performance mode not supported")
}
}
override fun getLibraries(): Array<String> = arrayOf(
"SDL3",
"SDL3_ttf",
"gopher64"
)
override fun getMainFunction(): String = "gopher64_sdl_main"
override fun getArguments(): Array<String> {
val intent = intent ?: return super.getArguments()
val args = intent.getStringArrayExtra("args") ?: return super.getArguments()
val dataIntent = Intent()
val file_path = intent.getStringExtra("file_path")
if (file_path != null) {
dataIntent.putExtra("file_path", file_path)
}
val cheats_path = intent.getStringExtra("cheats_path")
if (cheats_path != null) {
dataIntent.putExtra("cheats_path", cheats_path)
}
setResult(RESULT_OK, dataIntent)
return args
}
}
@@ -0,0 +1,19 @@
package io.github.gopher64.gopher64
import android.app.NativeActivity
import android.content.Intent
class SlintActivity : NativeActivity() {
companion object {
init {
System.loadLibrary("gopher64")
}
}
private external fun nativeOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
nativeOnActivityResult(requestCode, resultCode, data)
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#419567</color>
</resources>
+4
View File
@@ -0,0 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}
+15
View File
@@ -0,0 +1,15 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
@@ -0,0 +1,12 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
toolchainVersion=21
+20
View File
@@ -0,0 +1,20 @@
[versions]
agp = "9.2.1"
coreKtx = "1.18.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.14.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Binary file not shown.
@@ -0,0 +1,9 @@
#Thu May 21 08:40:22 CEST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Gopher64"
include(":app")
+35 -12
View File
@@ -2,10 +2,13 @@ fn main() {
println!("cargo::rerun-if-changed=parallel-rdp");
println!("cargo::rerun-if-changed=retroachievements");
println!("cargo::rerun-if-changed=src/compat");
println!("cargo::rerun-if-changed=data/translations");
println!("cargo::rerun-if-changed=data/ui");
#[cfg(feature = "gui")]
{
let slint_config = slint_build::CompilerConfiguration::new();
let slint_config = slint_build::CompilerConfiguration::new()
.with_bundled_translations("data/translations");
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
}
@@ -133,9 +136,6 @@ fn main() {
.compile()
.unwrap();
} else if os == "macos" {
println!("cargo:rustc-link-search=native=/opt/homebrew/opt/freetype/lib");
println!("cargo:rustc-link-lib=dylib=freetype");
let output = std::process::Command::new("clang")
.args(["--print-runtime-dir"])
.output()
@@ -146,9 +146,6 @@ fn main() {
println!("cargo:rustc-link-search=native={}", runtime_dir);
println!("cargo:rustc-link-lib=static=clang_rt.osx");
}
if cfg!(not(feature = "gui")) && os == "linux" {
println!("cargo:rustc-link-lib=dylib=freetype");
}
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
@@ -169,7 +166,7 @@ fn main() {
.allowlist_function("ra_get_hardcore")
.allowlist_function("ra_load_game")
.allowlist_function("ra_unload_game")
.allowlist_function("ra_set_dmem")
.allowlist_function("ra_set_rdram")
.allowlist_function("ra_do_frame")
.allowlist_function("ra_do_idle")
.allowlist_function("ra_http_callback")
@@ -177,13 +174,13 @@ fn main() {
.allowlist_function("ra_login_token_user")
.allowlist_function("ra_state_size")
.allowlist_function("ra_save_state")
.allowlist_function("ra_load_state");
.allowlist_function("ra_load_state")
.allowlist_function("ra_get_rich_presence");
if cfg!(feature = "gui") {
retroachievements_builder = retroachievements_builder
.allowlist_function("ra_logout_user")
.allowlist_function("ra_get_username")
.allowlist_function("ra_get_token");
.allowlist_function("ra_get_username");
}
let retroachievements_bindings = retroachievements_builder
@@ -205,12 +202,13 @@ fn main() {
.allowlist_function("rdp_process_commands")
.allowlist_function("rdp_onscreen_message")
.allowlist_function("rdp_check_callback")
.allowlist_function("rdp_new_processor")
.allowlist_function("rdp_check_framebuffers")
.allowlist_function("rdp_state_size")
.allowlist_function("rdp_save_state")
.allowlist_function("rdp_load_state")
.allowlist_function("rdp_idle")
.allowlist_function("rdp_set_fps")
.allowlist_function("get_joystick_event")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
@@ -308,4 +306,29 @@ fn main() {
{
println!("cargo:rustc-cfg=ra_hardcore_enabled");
}
if os == "android" {
copy_dir_all(
std::path::Path::new(sdl3_src::SOURCE_DIR)
.join("android-project/app/src/main/java/org/libsdl/app"),
"android-project/app/src/main/java/org/libsdl/app",
)
.unwrap();
}
}
fn copy_dir_all(
src: impl AsRef<std::path::Path>,
dst: impl AsRef<std::path::Path>,
) -> std::io::Result<()> {
std::fs::create_dir_all(&dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>
+26
View File
@@ -0,0 +1,26 @@
# Adding translations to Gopher64
1. Copy and modify the `gopher64.pot` file in this directory with your translations.
2. Rename it to `gopher64.po` and put it here:
`data/translations/<lang>/LC_MESSAGES/gopher64.po`
See [here](https://docs.slint.dev/latest/docs/slint/guide/development/translations/#translating-strings) for more details on creating a .po file. I recommend using a program like [poedit](https://poedit.com) to create the .po file from the template.
---
For example, in the .pot file you will see:
```
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr ""
```
A translation will look something like this:
```
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "Nova versão disponível! Clique aqui para baixar"
```
The only line you modify is the `msgstr`. The `msgctxt` and `msgid` need to remain unchanged.
@@ -0,0 +1,620 @@
# gopher64 - German Translation
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "Neue Version verfügbar! Zum Herunterladen hier klicken"
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr "Über"
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr "Wiki"
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr "Discord-Server"
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr "Quellcode"
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr "Spenden (über Patreon)"
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr "Spenden (über GitHub)"
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Lokales Spiel"
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Controller-Konfiguration"
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Einstellungen"
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Über"
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr "ROM auswählen"
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr "Cheats für alle ROMs löschen"
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr "Keine Cheats verfügbar"
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr "Aktiviert"
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr "Transfer Pak"
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr "Profil"
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr "Controller-Konfiguration"
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr "Eingabeprofil konfigurieren"
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr "Controller-Einstellungen"
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr "Controller-Zuweisung"
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr "Eingabeprofil erstellen/bearbeiten"
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr "Profilname zum Erstellen oder Bearbeiten eingeben"
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr "Deadzone Größe in Prozent"
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr "DirectInput verwenden"
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr "Assistent zur Profilerstellung starten"
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr "Abbrechen"
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr "Lokales Spiel"
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr "ROM öffnen"
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr "Speicherordner öffnen"
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr "Zuletzt verwendete ROMs"
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr "Netplay: Sitzung erstellen"
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr "Sitzungsname:"
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr "Sitzungsname eingeben"
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr "Passwort (optional):"
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr "Sitzungspasswort eingeben"
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr "Spielername:"
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr "Spielername eingeben"
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr "ROM auswählen"
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr ""
"Für dieses Spiel sind Cheats aktiviert. Cheats werden mit anderen Spielern "
"synchronisiert."
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Sitzung erstellen"
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Abbrechen"
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr "Netplay: Sitzung beitreten"
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr "Spielername (erforderlich):"
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr "Spielername eingeben"
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr "Passwort (falls erforderlich)"
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr "ROM auswählen"
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr "Sitzungsliste wird aktualisiert, bitte warten …"
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr "Auf eine Sitzung klicken um diese auszuwählen"
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr "Keine Sitzungen verfügbar. Bitte aktualisieren, um erneut zu prüfen."
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr "Sitzungsname"
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr "Spielname"
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr "Passwort"
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr "Sitzungsliste aktualisieren"
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr "Sitzung beitreten"
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr "Abbrechen"
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr "Sitzung erstellen"
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr "Sitzung beitreten"
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr "Netplay-Discord-Kanal"
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Feedback geben"
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: Warteraum"
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Sitzungsname:"
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Spielname:"
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Spieler 1:"
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Spieler 2:"
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Spieler 3:"
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Spieler 4:"
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Chatnachricht eingeben"
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Nachricht senden"
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\n"
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
"Alt+]."
msgstr ""
"Eine zu niedrige Eingabeverzögerung führt zu Ruckeln im Spiel.\n"
"Sie können die Eingabeverzögerung im Spiel mit Alt+[ verringern und mit "
"Alt+] erhöhen."
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Lokale Eingabeverzögerung:"
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Empfohlen: "
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Spiel starten"
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Warten auf P2P-Mesh "
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Abbrechen"
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr "Derzeit nicht angemeldet"
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr "Aktiviert"
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr "Hardcore-Modus"
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr "Challenge Tipps"
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr "Bestenlisten-Tracker"
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr "Rich Presence für Discord"
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr "Profil anzeigen"
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr "Benutzername"
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr "Passwort"
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr "Anmelden"
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Unterstützte Spiele anzeigen"
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Einstellungen"
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr "Spiel"
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr "Auflösung:"
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAA-Herunterskalierung"
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Pixelgenaue Skalierung"
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Vollbild"
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "16:9 (gestreckt)"
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "VSync"
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "CRT-Shader anwenden"
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "N64-CPU übertakten (kann Fehler verursachen)"
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Expansion Pak deaktivieren"
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Rewind aktivieren"
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "UNFLoader emulieren"
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr "VRU emulieren"
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr "Benutzeroberfläche"
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr "Thema:"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr "System"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr "Dunkel"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr "Hell"
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr "Was möchten Sie sagen?"
#~ msgctxt "NetplayCreate"
#~ msgid "Server:"
#~ msgstr "Server:"
#~ msgctxt "CustomServerDialog"
#~ msgid "Enter a custom server URL"
#~ msgstr "Eigene Server-URL eingeben"
#~ msgctxt "NetplayJoin"
#~ msgid "Custom Server URL (optional):"
#~ msgstr "Eigene Server-URL (optional):"
#~ msgctxt "NetplayJoin"
#~ msgid "Server"
#~ msgstr "Server"
#~ msgctxt "NetplayWait"
#~ msgid "Ping:"
#~ msgstr "Ping:"
#~ msgctxt "NetplayCreate"
#~ msgid "Creating server, please wait...This may take about 30 seconds."
#~ msgstr ""
#~ "Server wird erstellt, bitte warten … Dies kann etwa 30 Sekunden dauern."
#~ msgctxt "ControllerConfig"
#~ msgid "Port"
#~ msgstr "Port"
#~ msgctxt "ControllerConfig"
#~ msgid "Controller"
#~ msgstr "Controller"
@@ -0,0 +1,620 @@
# gopher64 - Spanish Translation
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: Gemini\n"
"Language-Team: Spanish (Spain)\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "¡Nueva versión disponible! Pulsa aquí para descargar"
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr "Acerca de"
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr "Wiki"
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr "Servidor de Discord"
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr "Código fuente"
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr "Donar (a través de Patreon)"
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr "Donar (a través de GitHub)"
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Juego local"
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Config. del mando"
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Trucos"
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Ajustes"
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Acerca de"
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr "Trucos"
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr "Seleccionar ROM"
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr "Borrar trucos de todas las ROMs"
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr "No hay trucos disponibles"
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr "Activado"
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr "Transfer Pak"
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr "Perfil"
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr "Configuración del mando"
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr "Configurar perfil de entrada"
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr "Ajustes del mando"
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr "Asignación del mando"
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr "Crear/Modificar perfil de entrada"
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr "Introduce el nombre del perfil para crear o modificar"
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr "Porcentaje de zona muerta"
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr "Usar DirectInput"
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr "Iniciar asistente de creación de perfil"
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr "Juego local"
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr "Abrir ROM"
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr "Abrir carpeta de guardados"
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr "ROMs recientes"
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr "Netplay: Crear sesión"
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr "Nombre de la sesión:"
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr "Introduce el nombre de la sesión"
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr "Contraseña (Opcional):"
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr "Introduce la contraseña"
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr "Nombre del jugador:"
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr "Introduce tu nombre"
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr "Seleccionar ROM"
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr ""
"Este juego tiene trucos activados. Los trucos se sincronizarán con los otros "
"jugadores."
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Crear sesión"
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr "Netplay: Unirse a la sesión"
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr "Nombre de jugador (requerido):"
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr "Introduce tu nombre"
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr "Contraseña (si es necesaria)"
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr "Seleccionar ROM"
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr "Actualizando lista de sesiones, espera..."
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr "Pulsa una sesión para seleccionarla"
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr "No hay sesiones disponibles. Actualiza para comprobar de nuevo."
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr "Nombre de la sesión"
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr "Nombre del juego"
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr "Contraseña"
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr "Trucos"
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr "Actualizar lista"
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr "Unirse a la sesión"
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr "Crear sesión"
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr "Unirse a la sesión"
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr "Canal de Discord de Netplay"
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Enviar comentarios"
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: Sala de espera"
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Nombre de la sesión:"
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Nombre del juego:"
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Jugador 1:"
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Jugador 2:"
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Jugador 3:"
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Jugador 4:"
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Escribe un mensaje en el chat"
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Enviar mensaje"
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\n"
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
"Alt+]."
msgstr ""
"Configurar un retardo de entrada demasiado bajo hará que el juego se "
"ralentice.\n"
"Puedes reducir el retardo de entrada en el juego pulsando Alt+[ y aumentarlo "
"con Alt+]."
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Retardo de entrada local:"
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Iniciar juego"
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Esperando malla P2P "
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr "Sesión no iniciada"
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr "Activado"
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr "Modo Hardcore"
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr "Indicadores de desafío"
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr "Indicadores de tabla de clasificación"
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr "Rich Presence en Discord"
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr "Ver perfil"
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr "Usuario"
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr "Contraseña"
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr "Entrar"
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Ver juegos compatibles"
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Ajustes"
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr "Juego"
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr "Resolución:"
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "Reducción de escala SSAA"
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Escalado por enteros"
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Pantalla completa"
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Pantalla ancha (estirado)"
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "Sincronización vertical (VSync)"
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Aplicar sombreador CRT"
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Overclock CPU N64 (puede causar errores)"
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Desactivar Expansion Pak"
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Activar rebobinado"
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Emular UNFLoader"
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr "Emular VRU"
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr "Interfaz"
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr "Tema:"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr "Sistema"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr "Oscuro"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr "Claro"
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr "¿Qué te gustaría decir?"
#~ msgctxt "NetplayCreate"
#~ msgid "Server:"
#~ msgstr "Servidor:"
#~ msgctxt "CustomServerDialog"
#~ msgid "Enter a custom server URL"
#~ msgstr "Introduce la URL de servidor personalizado"
#~ msgctxt "NetplayJoin"
#~ msgid "Custom Server URL (optional):"
#~ msgstr "URL de servidor personalizado (opcional):"
#~ msgctxt "NetplayJoin"
#~ msgid "Server"
#~ msgstr "Servidor"
#~ msgctxt "NetplayWait"
#~ msgid "Ping:"
#~ msgstr "Latencia (Ping):"
#~ msgctxt "NetplayCreate"
#~ msgid "Creating server, please wait...This may take about 30 seconds."
#~ msgstr "Creando servidor, espera... Puede tardar unos 30 segundos."
#~ msgctxt "ControllerConfig"
#~ msgid "Port"
#~ msgstr "Puerto"
#~ msgctxt "ControllerConfig"
#~ msgid "Controller"
#~ msgstr "Mando"
@@ -0,0 +1,622 @@
# gopher64 - French (France) Translation
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: French (France)\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "Nouvelle version disponible ! Cliquez ici pour télécharger"
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr "À propos"
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr "Wiki"
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr "Serveur Discord"
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr "Code source"
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr "Faire un don (via Patreon)"
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr "Faire un don (via GitHub)"
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Jeu local"
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Configuration manette"
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Triche"
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Paramètres"
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "À propos"
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr "Triche"
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr "Sélectionner une ROM"
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr "Effacer la triche pour toutes les ROMs"
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr "Aucune triche disponible"
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr "Activé"
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr "Transfer Pak"
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr "Profil"
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr "Configuration de la manette"
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr "Configurer le profil d'entrée"
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr "Paramètres de la manette"
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr "Attribution de la manette"
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr "Créer / modifier le profil d'entrée"
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr "Saisir le nom du profil à créer ou à modifier"
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr "Pourcentage de zone morte"
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr "Utiliser DirectInput"
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr "Lancer l'assistant de création de profil"
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr "Annuler"
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr "Jeu local"
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr "Ouvrir une ROM"
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr "Ouvrir le dossier des sauvegardes"
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr "ROMs récentes"
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr "Netplay : créer une session"
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr "Nom de la session :"
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr "Saisir le nom de la session"
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr "Mot de passe (facultatif) :"
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr "Saisir le mot de passe de la session"
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr "Nom du joueur :"
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr "Saisir le nom du joueur"
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr "Sélectionner une ROM"
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr ""
"La triche est activée pour ce jeu. Elle sera synchronisée avec les autres "
"joueurs."
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Créer la session"
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Annuler"
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr "Netplay : rejoindre une session"
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr "Nom du joueur (obligatoire) :"
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr "Saisir le nom du joueur"
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr "Mot de passe (si requis)"
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr "Sélectionner une ROM"
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr "Actualisation de la liste des sessions, veuillez patienter…"
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr "Cliquez sur une session ci-dessous pour la sélectionner"
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr ""
"Aucune session disponible. Veuillez actualiser pour vérifier à nouveau."
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr "Nom de la session"
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr "Nom du jeu"
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr "Mot de passe"
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr "Triche"
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr "Actualiser la liste"
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr "Rejoindre la session"
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr "Annuler"
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr "Créer une session"
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr "Rejoindre une session"
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr "Salon Discord Netplay"
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Donner votre avis"
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay : salle d'attente"
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Nom de la session :"
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Nom du jeu :"
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Joueur 1 :"
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Joueur 2 :"
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Joueur 3 :"
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Joueur 4 :"
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Saisir un message"
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Envoyer"
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\n"
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
"Alt+]."
msgstr ""
"Régler le délai d'entrée trop bas provoquera des ralentissements.\n"
"Vous pouvez diminuer le délai d'entrée en jeu avec Alt+[ et l'augmenter avec "
"Alt+]."
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Délai d'entrée local :"
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recommandé : "
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Lancer la partie"
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "En attente du maillage P2P "
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Annuler"
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr "Non connecté"
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr "Activé"
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr "Mode hardcore"
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr "Indicateurs de défi"
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr "Suivi des classements"
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr "Rich Presence Discord"
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr "Voir le profil"
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr "Nom d'utilisateur"
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr "Mot de passe"
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr "Connexion"
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Voir les jeux pris en charge"
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Paramètres"
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr "Jeu"
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr "Résolution :"
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "Réduction SSAA"
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Mise à l'échelle par entiers"
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Plein écran"
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Écran large (étiré)"
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "Synchro verticale"
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Appliquer le filtre CRT"
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Surcharger le CPU N64 (risque de bugs)"
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Désactiver l'Expansion Pak"
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Activer le rembobinage"
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Émuler UNFLoader"
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr "Émuler le VRU"
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr "Interface"
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr "Thème :"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr "Système"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr "Sombre"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr "Clair"
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr "Que souhaitez-vous dire ?"
#~ msgctxt "NetplayCreate"
#~ msgid "Server:"
#~ msgstr "Serveur :"
#~ msgctxt "CustomServerDialog"
#~ msgid "Enter a custom server URL"
#~ msgstr "Saisir l'URL personnalisée du serveur"
#~ msgctxt "NetplayJoin"
#~ msgid "Custom Server URL (optional):"
#~ msgstr "URL personnalisée du serveur (facultatif) :"
#~ msgctxt "NetplayJoin"
#~ msgid "Server"
#~ msgstr "Serveur"
#~ msgctxt "NetplayWait"
#~ msgid "Ping:"
#~ msgstr "Ping :"
#~ msgctxt "NetplayCreate"
#~ msgid "Creating server, please wait...This may take about 30 seconds."
#~ msgstr ""
#~ "Création du serveur, veuillez patienter… Cela peut prendre environ 30 "
#~ "secondes."
#~ msgctxt "ControllerConfig"
#~ msgid "Port"
#~ msgstr "Port"
#~ msgctxt "ControllerConfig"
#~ msgid "Controller"
#~ msgstr "Manette"
+581
View File
@@ -0,0 +1,581 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr ""
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr ""
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr ""
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr ""
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr ""
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr ""
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr ""
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr ""
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr ""
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr ""
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr ""
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr ""
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr ""
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr ""
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr ""
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr ""
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr ""
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr ""
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr ""
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr ""
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr ""
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr ""
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr ""
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr ""
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr ""
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr ""
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr ""
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr ""
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr ""
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr ""
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr ""
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr ""
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr ""
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr ""
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr ""
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr ""
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr ""
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr ""
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr ""
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr ""
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr ""
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr ""
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr ""
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr ""
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr ""
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr ""
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr ""
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr ""
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr ""
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr ""
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr ""
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr ""
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr ""
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr ""
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr ""
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr ""
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr ""
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr ""
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr ""
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr ""
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr ""
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr ""
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr ""
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr ""
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr ""
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr ""
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr ""
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr ""
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr ""
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\nYou can lower "
"the input delay in-game by pressing Alt+[ and raise it with Alt+]."
msgstr ""
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr ""
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr ""
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr ""
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr ""
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr ""
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr ""
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr ""
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr ""
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr ""
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr ""
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr ""
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr ""
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr ""
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr ""
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr ""
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr ""
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr ""
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr ""
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr ""
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr ""
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr ""
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr ""
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr ""
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr ""
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr ""
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr ""
@@ -0,0 +1,618 @@
# gopher64 - Japanese Translation
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "新しいバージョンがあります。クリックしてダウンロード"
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr "バージョン情報"
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr "Wiki"
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr "Discordサーバー"
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr "ソースコード"
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr "Patreonで寄付"
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr "GitHubで寄付"
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "ローカルゲーム"
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "コントローラー設定"
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "ネットプレイ"
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "チート"
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "設定"
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "バージョン情報"
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr "チート"
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr "ROMを選択"
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr "すべてのROMのチートをクリア"
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr "利用できるチートはありません"
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr "有効"
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr "Transfer Pak"
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr "プロファイル"
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr "コントローラー設定"
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr "入力プロファイルを設定"
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr "コントローラー詳細設定"
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr "コントローラー割り当て"
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr "入力プロファイルの作成/変更"
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr "作成または変更するプロファイル名を入力"
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr "デッドゾーン(%)"
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr "DirectInputを使用"
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr "プロファイル作成ウィザードを開始"
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr "キャンセル"
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr "ローカルゲーム"
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr "ROMを開く"
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr "セーブフォルダーを開く"
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr "最近使ったROM"
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr "ネットプレイ:セッション作成"
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr "セッション名:"
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr "セッション名を入力"
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr "パスワード(任意):"
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr "セッションのパスワードを入力"
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr "プレイヤー名:"
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr "プレイヤー名を入力"
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr "ROMを選択"
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr "このゲームではチートが有効です。チートは他のプレイヤーと同期されます。"
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "セッションを作成"
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "キャンセル"
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr "ネットプレイ:セッションに参加"
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr "プレイヤー名(必須):"
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr "プレイヤー名を入力"
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr "パスワード(必要な場合)"
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr "ROMを選択"
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr "セッション一覧を更新しています。しばらくお待ちください…"
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr "下の一覧からセッションをクリックして選択"
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr "利用できるセッションがありません。更新して再度確認してください。"
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr "セッション名"
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr "ゲーム名"
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr "パスワード"
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr "チート"
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr "セッション一覧を更新"
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr "セッションに参加"
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr "キャンセル"
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr "ネットプレイ"
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr "セッションを作成"
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr "セッションに参加"
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr "ネットプレイ用Discordチャンネル"
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "フィードバックを送る"
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "ネットプレイ:待機室"
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "セッション名:"
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "ゲーム名:"
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "プレイヤー1"
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "プレイヤー2"
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "プレイヤー3"
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "プレイヤー4"
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "チャットメッセージを入力"
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "送信"
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\n"
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
"Alt+]."
msgstr ""
"入力遅延を低く設定しすぎるとゲームが遅延します。\n"
"Alt+[ でゲーム内の入力遅延を下げ、Alt+] で上げることができます。"
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "ローカル入力遅延:"
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "推奨: "
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "ゲーム開始"
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "P2Pメッシュ接続待機中 "
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "キャンセル"
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr "ログインしていません"
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr "有効"
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr "ハードコアモード"
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr "チャレンジ表示"
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr "リーダーボード追跡"
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr "Discord のリッチプレゼンス"
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr "プロフィールを表示"
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr "ユーザー名"
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr "パスワード"
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr "ログイン"
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "対応ゲームを表示"
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "設定"
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr "ゲーム"
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr "解像度:"
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAAダウンスケール"
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "整数倍スケーリング"
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "全画面表示"
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "ワイド画面(伸ばす)"
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "垂直同期(VSync"
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "CRTシェーダーを適用"
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "N64のCPUをオーバークロック(不具合の原因になることがあります)"
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "拡張パックを無効にする"
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "巻き戻しを有効にする"
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "UNFLoaderをエミュレート"
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr "VRUをエミュレート"
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr "UI"
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr "テーマ:"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr "システム"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr "ダーク"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr "ライト"
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr "何と言いますか?"
#~ msgctxt "NetplayCreate"
#~ msgid "Server:"
#~ msgstr "サーバー:"
#~ msgctxt "CustomServerDialog"
#~ msgid "Enter a custom server URL"
#~ msgstr "カスタムサーバーURLを入力"
#~ msgctxt "NetplayJoin"
#~ msgid "Custom Server URL (optional):"
#~ msgstr "カスタムサーバーURL(任意):"
#~ msgctxt "NetplayJoin"
#~ msgid "Server"
#~ msgstr "サーバー"
#~ msgctxt "NetplayWait"
#~ msgid "Ping:"
#~ msgstr "Ping"
#~ msgctxt "NetplayCreate"
#~ msgid "Creating server, please wait...This may take about 30 seconds."
#~ msgstr ""
#~ "サーバーを作成しています。しばらくお待ちください… 最大約30秒かかることがあ"
#~ "ります。"
#~ msgctxt "ControllerConfig"
#~ msgid "Port"
#~ msgstr "ポート"
#~ msgctxt "ControllerConfig"
#~ msgid "Controller"
#~ msgstr "コントローラー"
@@ -0,0 +1,620 @@
# gopher64 - Dutch (Netherlands) Translation
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: Dutch (Netherlands)\n"
"Language: nl_NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "Nieuwe versie beschikbaar! Klik hier om te downloaden"
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr "Over"
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr "Wiki"
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr "Discord-server"
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr "Broncode"
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr "Doneren (via Patreon)"
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr "Doneren (via GitHub)"
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Lokaal spel"
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Controllerconfiguratie"
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Instellingen"
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Over"
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr "ROM selecteren"
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr "Cheats wissen voor alle ROM's"
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr "Geen cheats beschikbaar"
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr "Ingeschakeld"
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr "Transfer Pak"
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr "Profiel"
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr "Controllerconfiguratie"
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr "Invoerprofiel configureren"
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr "Controllerinstellingen"
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr "Controllertoewijzing"
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr "Invoerprofiel maken/wijzigen"
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr "Profielnaam invoeren om aan te maken of te wijzigen"
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr "Dode-zonepercentage"
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr "DirectInput gebruiken"
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr "Wizard voor profiel maken starten"
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr "Annuleren"
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr "Lokaal spel"
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr "ROM openen"
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr "Map met opslagen openen"
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr "Recent gebruikte ROM's"
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr "Netplay: sessie aanmaken"
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr "Sessienaam:"
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr "Sessienaam invoeren"
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr "Wachtwoord (optioneel):"
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr "Sessiewachtwoord invoeren"
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr "Spelernaam:"
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr "Spelernaam invoeren"
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr "ROM selecteren"
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr ""
"Voor dit spel zijn cheats ingeschakeld. Cheats worden gesynchroniseerd met "
"andere spelers."
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Sessie aanmaken"
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Annuleren"
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr "Netplay: deelnemen aan sessie"
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr "Spelernaam (verplicht):"
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr "Spelernaam invoeren"
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr "Wachtwoord (indien vereist)"
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr "ROM selecteren"
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr "Sessielijst wordt vernieuwd, even geduld…"
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr "Klik hieronder op een sessie om die te selecteren"
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr "Geen sessies beschikbaar. Vernieuw om opnieuw te controleren."
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr "Sessienaam"
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr "Spelnaam"
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr "Wachtwoord"
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr "Cheats"
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr "Sessielijst vernieuwen"
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr "Deelnemen aan sessie"
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr "Annuleren"
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr "Sessie aanmaken"
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr "Deelnemen aan sessie"
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr "Netplay-Discordkanaal"
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Feedback geven"
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: wachtruimte"
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Sessienaam:"
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Spelnaam:"
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Speler 1:"
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Speler 2:"
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Speler 3:"
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Speler 4:"
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Chatbericht invoeren"
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Bericht verzenden"
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\n"
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
"Alt+]."
msgstr ""
"Een te lage invoervertraging zorgt ervoor dat het spel gaat haperen.\n"
"Je kunt de invoervertraging in het spel verlagen met Alt+[ en verhogen met "
"Alt+]."
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Lokale invoervertraging:"
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Aanbevolen: "
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Spel starten"
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Wachten op P2P-mesh "
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Annuleren"
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr "Momenteel niet ingelogd"
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr "Ingeschakeld"
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr "Hardcore-modus"
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr "Challenge-indicatoren"
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr "Leaderboard-trackers"
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr "Discord Rich Presence"
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr "Profiel bekijken"
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr "Gebruikersnaam"
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr "Wachtwoord"
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr "Inloggen"
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Ondersteunde spellen bekijken"
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Instellingen"
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr "Spel"
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr "Resolutie:"
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAA-downscaling"
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Schalen in hele getallen"
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Volledig scherm"
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Breedbeeld (uitgerekt)"
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "VSync"
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "CRT-shader toepassen"
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "N64-CPU overklokken (kan bugs veroorzaken)"
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Expansion Pak uitschakelen"
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Terugspoelen inschakelen"
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "UNFLoader emuleren"
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr "VRU emuleren"
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr "Interface"
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr "Thema:"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr "Systeem"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr "Donker"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr "Licht"
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr "Wat wil je zeggen?"
#~ msgctxt "NetplayCreate"
#~ msgid "Server:"
#~ msgstr "Server:"
#~ msgctxt "CustomServerDialog"
#~ msgid "Enter a custom server URL"
#~ msgstr "Aangepaste server-URL invoeren"
#~ msgctxt "NetplayJoin"
#~ msgid "Custom Server URL (optional):"
#~ msgstr "Aangepaste server-URL (optioneel):"
#~ msgctxt "NetplayJoin"
#~ msgid "Server"
#~ msgstr "Server"
#~ msgctxt "NetplayWait"
#~ msgid "Ping:"
#~ msgstr "Ping:"
#~ msgctxt "NetplayCreate"
#~ msgid "Creating server, please wait...This may take about 30 seconds."
#~ msgstr ""
#~ "Server wordt aangemaakt, even geduld… Dit kan ongeveer 30 seconden duren."
#~ msgctxt "ControllerConfig"
#~ msgid "Port"
#~ msgstr "Poort"
#~ msgctxt "ControllerConfig"
#~ msgid "Controller"
#~ msgstr "Controller"
@@ -0,0 +1,621 @@
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 18:11-3000\n"
"Last-Translator: EDUARDO CARVALHO <eduardotcarvalho@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: Brazilian Portuguese\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "Nova versão disponível! Clique aqui para baixar"
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr "Sobre"
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr "Wiki"
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr "Servidor do Discord"
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr "Código Fonte"
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr "Doações (via Patreon)"
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr "Doações (via GitHub)"
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Jogo Local"
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Configurar Controle"
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Trapaças"
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Configurações"
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "Sobre"
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr "Trapaças"
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr "Selecionar ROM"
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr "Limpar Trapaças para todas as ROMs"
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr "Sem trapaças disponíveis"
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr "Habilitado"
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr "Transfer Pak"
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr "Perfil"
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr "Configurar Controles"
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr "Configurar Perfil de Controle"
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr "Configurações do controle"
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr "Atribuição do controle"
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr "Criar/Modificar Perfil de Comandos"
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr "Insira o nome de perfil para criar ou modificar"
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr "Porcentagem de Zona-morta"
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr "Usar DirectInput"
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr "Iniciar Assistente de Criação de Perfil"
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr "Jogo Local"
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr "Abrir ROM"
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr "Abrir Pasta de Saves"
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr "ROMs Recentes"
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr "Netplay: Criar Sessão"
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr "Nome da Sessão:"
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr "Insira nome da sessão"
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr "Senha: (Opcional):"
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr "Insira a senha da sessão"
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr "Nome de Jogador:"
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr "Insira nome de jogador"
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr "Selecionar ROM"
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr ""
"Este jogo tem trapaças habilitadas. As trapaças serão sincronizadas com "
"outros jogadores."
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Criar Sessão"
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr "Netplay: Entrar na sessão"
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr "Nome de Jogador (obrigatório):"
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr "Insira o nome de jogador"
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr "Senha (caso obrigatória)"
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr "Selecionar ROM"
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr "Recarregando lista de sessões, por favor aguarde..."
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr "Clique em uma sessão abaixo para selecionar"
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr ""
"Sem sessões disponíveis. Por favor recarregue para verificar novamente."
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr "Nome da Sessão"
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr "Nome do Jogo"
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr "Senha"
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr "Trapaças"
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr "Recarregar Lista de Sessões"
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr "Entrar na Sessão"
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr "Netplay"
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr "Criar Sessão"
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr "Entrar na Sessão"
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr "Canal de Netplay do Discord"
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Fornecer Feedback"
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Netplay: Aguardando Sala"
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Nome da Sessão:"
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Nome do Jogo:"
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Jogador 1:"
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Jogador 2:"
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Jogador 3:"
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Jogador 4:"
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Escrever mensagem no chat"
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Enviar Mensagem"
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\n"
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
"Alt+]."
msgstr ""
"Definir o atraso de entrada muito baixo causará lag no jogo.\n"
"Você pode diminuir o atraso de entrada no jogo pressionando Alt+[ e aumentá-"
"lo com Alt+]."
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Atraso de entrada local:"
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Recomendado: "
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Iniciar jogo"
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Aguardando malha P2P "
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Cancelar"
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr "Não está logado."
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr "Habilitado"
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr "Modo Hardcore"
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr "Indicadores de Desafio"
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr "Rastreadores de Tabela"
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr "Rich Presence no Discord"
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr "Ver Perfil"
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr "Usuário"
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr "Senha"
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr "Login"
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Ver Jogos com Suporte"
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Configurações"
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr "Jogo"
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr "Resolução:"
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "Redução de SSAA"
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Proporção Íntegra"
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Tela Cheia"
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Widescreen (esticar)"
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "VSync"
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Aplicar filtro de CRT"
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Overclock da CPU do N64 (pode causar bugs)"
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Desabilitar Expansion Pak"
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Habilitar rebobinamento"
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Emular UNFLoader"
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr "Emular VRU"
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr "Interface"
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr "Tema:"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr "Sistema"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr "Escuro"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr "Claro"
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr "O que você quer falar?"
#~ msgctxt "NetplayCreate"
#~ msgid "Server:"
#~ msgstr "Servidor:"
#~ msgctxt "CustomServerDialog"
#~ msgid "Enter a custom server URL"
#~ msgstr "Insira a URL personalizada do servidor"
#~ msgctxt "NetplayJoin"
#~ msgid "Custom Server URL (optional):"
#~ msgstr "URL Personalizada de Servidor (opcional):"
#~ msgctxt "NetplayJoin"
#~ msgid "Server"
#~ msgstr "Servidor"
#~ msgctxt "NetplayWait"
#~ msgid "Ping:"
#~ msgstr "Ping:"
#~ msgctxt "NetplayCreate"
#~ msgid "Creating server, please wait...This may take about 30 seconds."
#~ msgstr ""
#~ "Criando servidor, por favor aguarde...Isto poderá levar até 30 segundos."
#~ msgctxt "ControllerConfig"
#~ msgid "Port"
#~ msgstr "Entrada"
#~ msgctxt "ControllerConfig"
#~ msgid "Controller"
#~ msgstr "Controle"
@@ -0,0 +1,621 @@
# gopher64 - Russian Translation
msgid ""
msgstr ""
"Project-Id-Version: gopher64\n"
"POT-Creation-Date: 2026-06-30 09:43+0000\n"
"PO-Revision-Date: 2026-05-11 12:00+0000\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: src/ui/gui/about_page.slint:17
msgctxt "NewVersionButton"
msgid "New version available! Click here to download"
msgstr "Доступна новая версия! Нажмите здесь для загрузки"
#: src/ui/gui/about_page.slint:33
msgctxt "About"
msgid "About"
msgstr "О программе"
#: src/ui/gui/about_page.slint:40
msgctxt "About"
msgid "Wiki"
msgstr "Вики"
#: src/ui/gui/about_page.slint:50
msgctxt "About"
msgid "Discord Server"
msgstr "Сервер Discord"
#: src/ui/gui/about_page.slint:60
msgctxt "About"
msgid "Source Code"
msgstr "Исходный код"
#: src/ui/gui/about_page.slint:70
msgctxt "About"
msgid "Donate (via Patreon)"
msgstr "Пожертвовать (через Patreon)"
#: src/ui/gui/about_page.slint:77
msgctxt "About"
msgid "Donate (via GitHub)"
msgstr "Пожертвовать (через GitHub)"
#: src/ui/gui/appwindow.slint:164
msgctxt "Menu"
msgid "Local Game"
msgstr "Локальная игра"
#: src/ui/gui/appwindow.slint:168
msgctxt "Menu"
msgid "Controller Config"
msgstr "Настройка контроллера"
#: src/ui/gui/appwindow.slint:172
msgctxt "Menu"
msgid "Netplay"
msgstr "Сетевая игра"
#: src/ui/gui/appwindow.slint:176
msgctxt "Menu"
msgid "Cheats"
msgstr "Читы"
#: src/ui/gui/appwindow.slint:180
msgctxt "Menu"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/appwindow.slint:184
msgctxt "Menu"
msgid "Settings"
msgstr "Настройки"
#: src/ui/gui/appwindow.slint:188
msgctxt "Menu"
msgid "About"
msgstr "О программе"
#: src/ui/gui/cheat_page.slint:15
msgctxt "Cheats"
msgid "Cheats"
msgstr "Читы"
#: src/ui/gui/cheat_page.slint:25
msgctxt "Cheats"
msgid "Select ROM"
msgstr "Выбрать ROM"
#: src/ui/gui/cheat_page.slint:41
msgctxt "Cheats"
msgid "Clear Cheats For All ROMs"
msgstr "Очистить читы для всех ROMов"
#: src/ui/gui/cheat_page.slint:83
msgctxt "Cheats"
msgid "No Cheats Available"
msgstr "Читы недоступны"
#: src/ui/gui/controller_page.slint:80
msgctxt "ControllerSettingsGrid"
msgid "Enabled"
msgstr "Включено"
#: src/ui/gui/controller_page.slint:93
msgctxt "ControllerSettingsGrid"
msgid "Transfer Pak"
msgstr "Transfer Pak"
#: src/ui/gui/controller_page.slint:107
msgctxt "ControllerSettingsGrid"
msgid "Profile"
msgstr "Профиль"
#: src/ui/gui/controller_page.slint:157
msgctxt "ControllerConfig"
msgid "Controller Configuration"
msgstr "Настройка контроллера"
#: src/ui/gui/controller_page.slint:171
msgctxt "ControllerConfig"
msgid "Configure Input Profile"
msgstr "Настроить профиль ввода"
#: src/ui/gui/controller_page.slint:180
msgctxt "ControllerConfig"
msgid "Controller Settings"
msgstr "Настройки контроллера"
#: src/ui/gui/controller_page.slint:190
msgctxt "ControllerConfig"
msgid "Controller Assignment"
msgstr "Назначение контроллера"
#: src/ui/gui/input_profile.slint:12
msgctxt "InputProfileDialog"
msgid "Create/Modify Input Profile"
msgstr "Создать/изменить профиль ввода"
#: src/ui/gui/input_profile.slint:25
msgctxt "InputProfileDialog"
msgid "Enter profile name to create or modify"
msgstr "Введите имя профиля для создания или изменения"
#: src/ui/gui/input_profile.slint:38
msgctxt "InputProfileDialog"
msgid "Deadzone Percentage"
msgstr "Процент мёртвой зоны"
#: src/ui/gui/input_profile.slint:61
msgctxt "InputProfileDialog"
msgid "Use DirectInput"
msgstr "Использовать DirectInput"
#: src/ui/gui/input_profile.slint:74
msgctxt "InputProfileDialog"
msgid "Start Profile Creation Wizard"
msgstr "Запустить мастер создания профиля"
#: src/ui/gui/input_profile.slint:82
msgctxt "InputProfileDialog"
msgid "Cancel"
msgstr "Отмена"
#: src/ui/gui/localgame_page.slint:19
msgctxt "LocalGame"
msgid "Local Game"
msgstr "Локальная игра"
#: src/ui/gui/localgame_page.slint:26
msgctxt "LocalGame"
msgid "Open ROM"
msgstr "Открыть ROM"
#: src/ui/gui/localgame_page.slint:37
msgctxt "LocalGame"
msgid "Open Saves Folder"
msgstr "Открыть папку сохранений"
#: src/ui/gui/localgame_page.slint:54
msgctxt "LocalGame"
msgid "Recent ROMs"
msgstr "Недавние ROMы"
#: src/ui/gui/netplay_create.slint:7
msgctxt "NetplayCreate"
msgid "Netplay: Create Session"
msgstr "Сетевая игра: Создать сессию"
#: src/ui/gui/netplay_create.slint:22
msgctxt "NetplayCreate"
msgid "Session Name:"
msgstr "Имя сессии:"
#: src/ui/gui/netplay_create.slint:26
msgctxt "NetplayCreate"
msgid "Enter session name"
msgstr "Введите имя сессии"
#: src/ui/gui/netplay_create.slint:36
msgctxt "NetplayCreate"
msgid "Password (Optional):"
msgstr "Пароль (необязательно):"
#: src/ui/gui/netplay_create.slint:40
msgctxt "NetplayCreate"
msgid "Enter session password"
msgstr "Введите пароль сессии"
#: src/ui/gui/netplay_create.slint:51
msgctxt "NetplayCreate"
msgid "Player Name:"
msgstr "Имя игрока:"
#: src/ui/gui/netplay_create.slint:55
msgctxt "NetplayCreate"
msgid "Enter player name"
msgstr "Введите имя игрока"
#: src/ui/gui/netplay_create.slint:66
msgctxt "NetplayCreate"
msgid "Select ROM"
msgstr "Выбрать ROM"
#: src/ui/gui/netplay_create.slint:82
msgctxt "NetplayCreate"
msgid ""
"This game has cheats enabled. Cheats will be synchronized with other players."
msgstr ""
"Для этой игры включены читы. Читы будут синхронизированы с другими игроками."
#: src/ui/gui/netplay_create.slint:91
msgctxt "NetplayCreate"
msgid "Create Session"
msgstr "Создать сессию"
#: src/ui/gui/netplay_create.slint:107
msgctxt "NetplayCreate"
msgid "Cancel"
msgstr "Отмена"
#: src/ui/gui/netplay_join.slint:7
msgctxt "NetplayJoin"
msgid "Netplay: Join Session"
msgstr "Сетевая игра: Присоединиться к сессии"
#: src/ui/gui/netplay_join.slint:18
msgctxt "NetplayJoin"
msgid "Player Name (required):"
msgstr "Имя игрока (обязательно):"
#: src/ui/gui/netplay_join.slint:22
msgctxt "NetplayJoin"
msgid "Enter player name"
msgstr "Введите имя игрока"
#: src/ui/gui/netplay_join.slint:31
msgctxt "NetplayJoin"
msgid "Password (if required)"
msgstr "Пароль (если требуется)"
#: src/ui/gui/netplay_join.slint:42
msgctxt "NetplayJoin"
msgid "Select ROM"
msgstr "Выбрать ROM"
#: src/ui/gui/netplay_join.slint:57
msgctxt "NetplayJoin"
msgid "Refreshing session list, please wait..."
msgstr "Обновление списка сессий, пожалуйста, подождите..."
#: src/ui/gui/netplay_join.slint:62
msgctxt "NetplayJoin"
msgid "Click on a session below to select it"
msgstr "Нажмите на сессию ниже, чтобы выбрать её"
#: src/ui/gui/netplay_join.slint:67
msgctxt "NetplayJoin"
msgid "No sessions available. Please refresh to check again."
msgstr "Нет доступных сессий. Пожалуйста, обновите, чтобы проверить снова."
#: src/ui/gui/netplay_join.slint:73
msgctxt "NetplayJoin"
msgid "Session Name"
msgstr "Имя сессии"
#: src/ui/gui/netplay_join.slint:74
msgctxt "NetplayJoin"
msgid "Game Name"
msgstr "Название игры"
#: src/ui/gui/netplay_join.slint:75
msgctxt "NetplayJoin"
msgid "Password"
msgstr "Пароль"
#: src/ui/gui/netplay_join.slint:76
msgctxt "NetplayJoin"
msgid "Cheats"
msgstr "Читы"
#: src/ui/gui/netplay_join.slint:90
msgctxt "NetplayJoin"
msgid "Refresh Session List"
msgstr "Обновить список сессий"
#: src/ui/gui/netplay_join.slint:105
msgctxt "NetplayJoin"
msgid "Join Session"
msgstr "Присоединиться к сессии"
#: src/ui/gui/netplay_join.slint:117
msgctxt "NetplayJoin"
msgid "Cancel"
msgstr "Отмена"
#: src/ui/gui/netplay_page.slint:31
msgctxt "Netplay"
msgid "Netplay"
msgstr "Сетевая игра"
#: src/ui/gui/netplay_page.slint:39
msgctxt "Netplay"
msgid "Create Session"
msgstr "Создать сессию"
#: src/ui/gui/netplay_page.slint:50
msgctxt "Netplay"
msgid "Join Session"
msgstr "Присоединиться к сессии"
#: src/ui/gui/netplay_page.slint:64
msgctxt "Netplay"
msgid "Netplay Discord Channel"
msgstr "Канал Discord для сетевой игры"
#: src/ui/gui/netplay_page.slint:71
msgctxt "Netplay"
msgid "Provide Feedback"
msgstr "Оставить отзыв"
#: src/ui/gui/netplay_wait.slint:17
msgctxt "NetplayWait"
msgid "Netplay: Waiting Room"
msgstr "Сетевая игра: Комната ожидания"
#: src/ui/gui/netplay_wait.slint:29
msgctxt "NetplayWait"
msgid "Session Name:"
msgstr "Имя сессии:"
#: src/ui/gui/netplay_wait.slint:35
msgctxt "NetplayWait"
msgid "Game Name:"
msgstr "Название игры:"
#: src/ui/gui/netplay_wait.slint:45
msgctxt "NetplayWait"
msgid "Player 1:"
msgstr "Игрок 1:"
#: src/ui/gui/netplay_wait.slint:49
msgctxt "NetplayWait"
msgid "Player 2:"
msgstr "Игрок 2:"
#: src/ui/gui/netplay_wait.slint:55
msgctxt "NetplayWait"
msgid "Player 3:"
msgstr "Игрок 3:"
#: src/ui/gui/netplay_wait.slint:59
msgctxt "NetplayWait"
msgid "Player 4:"
msgstr "Игрок 4:"
#: src/ui/gui/netplay_wait.slint:73
msgctxt "NetplayWait"
msgid "Enter chat message"
msgstr "Введите сообщение чата"
#: src/ui/gui/netplay_wait.slint:84
msgctxt "NetplayWait"
msgid "Send Message"
msgstr "Отправить сообщение"
#: src/ui/gui/netplay_wait.slint:94
msgctxt "NetplayWait"
msgid ""
"Setting the input delay too low will cause the game to lag.\n"
"You can lower the input delay in-game by pressing Alt+[ and raise it with "
"Alt+]."
msgstr ""
"Слишком низкая задержка ввода вызовет лаги в игре.\n"
"Вы можете уменьшить задержку ввода в игре, нажав Alt+[, и увеличить её с "
"помощью Alt+]."
#: src/ui/gui/netplay_wait.slint:101
msgctxt "NetplayWait"
msgid "Local Input Delay:"
msgstr "Локальная задержка ввода:"
#: src/ui/gui/netplay_wait.slint:118
msgctxt "NetplayWait"
msgid "Recommended: "
msgstr "Рекомендуется: "
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Start Game"
msgstr "Начать игру"
#: src/ui/gui/netplay_wait.slint:127
msgctxt "NetplayWait"
msgid "Waiting for P2P mesh "
msgstr "Ожидание P2P-сети "
#: src/ui/gui/netplay_wait.slint:135
msgctxt "NetplayWait"
msgid "Cancel"
msgstr "Отмена"
#: src/ui/gui/retroachievements_page.slint:19
msgctxt "RAData"
msgid "Not currently logged in"
msgstr "В данный момент не выполнен вход"
#: src/ui/gui/retroachievements_page.slint:27
msgctxt "RetroAchievements"
msgid "RetroAchievements"
msgstr "RetroAchievements"
#: src/ui/gui/retroachievements_page.slint:36
msgctxt "RetroAchievements"
msgid "Enabled"
msgstr "Включено"
#: src/ui/gui/retroachievements_page.slint:51
msgctxt "RetroAchievements"
msgid "Hardcore mode"
msgstr "Хардкорный режим"
#: src/ui/gui/retroachievements_page.slint:60
msgctxt "RetroAchievements"
msgid "Challenge Indicators"
msgstr "Индикаторы испытаний"
#: src/ui/gui/retroachievements_page.slint:69
msgctxt "RetroAchievements"
msgid "Leaderboard Trackers"
msgstr "Трекеры таблицы лидеров"
#: src/ui/gui/retroachievements_page.slint:78
msgctxt "RetroAchievements"
msgid "Discord Rich Presence"
msgstr "Расширенный статус Discord"
#: src/ui/gui/retroachievements_page.slint:96
msgctxt "RetroAchievements"
msgid "View Profile"
msgstr "Просмотреть профиль"
#: src/ui/gui/retroachievements_page.slint:106
msgctxt "RetroAchievements"
msgid "Username"
msgstr "Имя пользователя"
#: src/ui/gui/retroachievements_page.slint:116
msgctxt "RetroAchievements"
msgid "Password"
msgstr "Пароль"
#: src/ui/gui/retroachievements_page.slint:125
msgctxt "RetroAchievements"
msgid "Login"
msgstr "Войти"
#: src/ui/gui/retroachievements_page.slint:141
msgctxt "RetroAchievements"
msgid "View Supported Games"
msgstr "Просмотреть поддерживаемые игры"
#: src/ui/gui/settings_page.slint:25
msgctxt "Settings"
msgid "Settings"
msgstr "Настройки"
#: src/ui/gui/settings_page.slint:32
msgctxt "Settings"
msgid "Game"
msgstr "Игра"
#: src/ui/gui/settings_page.slint:38
msgctxt "Settings"
msgid "Resolution:"
msgstr "Разрешение:"
#: src/ui/gui/settings_page.slint:51
msgctxt "Settings"
msgid "SSAA downscaling"
msgstr "SSAA даунскейлинг"
#: src/ui/gui/settings_page.slint:59
msgctxt "Settings"
msgid "Integer Scaling"
msgstr "Целочисленное масштабирование"
#: src/ui/gui/settings_page.slint:67
msgctxt "Settings"
msgid "Fullscreen"
msgstr "Полноэкранный режим"
#: src/ui/gui/settings_page.slint:75
msgctxt "Settings"
msgid "Widescreen (stretch)"
msgstr "Широкоэкранный режим (растянуто)"
#: src/ui/gui/settings_page.slint:83
msgctxt "Settings"
msgid "VSync"
msgstr "Вертикальная синхронизация"
#: src/ui/gui/settings_page.slint:91
msgctxt "Settings"
msgid "Apply CRT shader"
msgstr "Применить шейдер ЭЛТ"
#: src/ui/gui/settings_page.slint:103
msgctxt "Settings"
msgid "Overclock N64 CPU (may cause bugs)"
msgstr "Разгон процессора N64 (может вызвать ошибки)"
#: src/ui/gui/settings_page.slint:111
msgctxt "Settings"
msgid "Disable Expansion Pak"
msgstr "Отключить Expansion Pak"
#: src/ui/gui/settings_page.slint:119
msgctxt "Settings"
msgid "Enable Rewind"
msgstr "Включить перемотку назад"
#: src/ui/gui/settings_page.slint:127
msgctxt "Settings"
msgid "Emulate UNFLoader"
msgstr "Эмулировать UNFLoader"
#: src/ui/gui/settings_page.slint:135
msgctxt "Settings"
msgid "Emulate VRU"
msgstr "Эмулировать VRU"
#: src/ui/gui/settings_page.slint:144
msgctxt "Settings"
msgid "UI"
msgstr "Интерфейс"
#: src/ui/gui/settings_page.slint:150
msgctxt "Settings"
msgid "Theme:"
msgstr "Тема:"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "System"
msgstr "Система"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Dark"
msgstr "Тёмная"
#: src/ui/gui/settings_page.slint:154
msgctxt "Settings"
msgid "Light"
msgstr "Светлая"
#: src/ui/gui/vru_dialog.slint:7
msgctxt "VruDialog"
msgid "What Would You Like To Say?"
msgstr "Что вы хотите сказать?"
#~ msgctxt "NetplayCreate"
#~ msgid "Server:"
#~ msgstr "Сервер:"
#~ msgctxt "CustomServerDialog"
#~ msgid "Enter a custom server URL"
#~ msgstr "Введите URL пользовательского сервера"
#~ msgctxt "NetplayJoin"
#~ msgid "Custom Server URL (optional):"
#~ msgstr "URL пользовательского сервера (необязательно):"
#~ msgctxt "NetplayJoin"
#~ msgid "Server"
#~ msgstr "Сервер"
#~ msgctxt "NetplayWait"
#~ msgid "Ping:"
#~ msgstr "Пинг:"
#~ msgctxt "NetplayCreate"
#~ msgid "Creating server, please wait...This may take about 30 seconds."
#~ msgstr ""
#~ "Создание сервера, пожалуйста, подождите... Это может занять около 30 "
#~ "секунд."
#~ msgctxt "ControllerConfig"
#~ msgid "Port"
#~ msgstr "Порт"
#~ msgctxt "ControllerConfig"
#~ msgid "Controller"
#~ msgstr "Контроллер"
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>

After

Width:  |  Height:  |  Size: 553 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M280-120v-80h160v-124q-49-11-87.5-41.5T296-442q-75-9-125.5-65.5T120-640v-40q0-33 23.5-56.5T200-760h80v-80h400v80h80q33 0 56.5 23.5T840-680v40q0 76-50.5 132.5T664-442q-18 46-56.5 76.5T520-324v124h160v80H280Zm0-408v-152h-80v40q0 38 22 68.5t58 43.5Zm285 93q35-35 35-85v-240H360v240q0 50 35 85t85 35q50 0 85-35Zm115-93q36-13 58-43.5t22-68.5v-40h-80v152Zm-200-52Z"/></svg>

After

Width:  |  Height:  |  Size: 483 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-160v-80h320v80H480ZM220-320l-56-56 183-184-183-184 56-56 240 240-240 240Z"/></svg>

After

Width:  |  Height:  |  Size: 203 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M189-160q-60 0-102.5-43T42-307q0-9 1-18t3-18l84-336q14-54 57-87.5t98-33.5h390q55 0 98 33.5t57 87.5l84 336q2 9 3.5 18.5T919-306q0 61-43.5 103.5T771-160q-42 0-78-22t-54-60l-28-58q-5-10-15-15t-21-5H385q-11 0-21 5t-15 15l-28 58q-18 38-54 60t-78 22Zm3-80q19 0 34.5-10t23.5-27l28-57q15-31 44-48.5t63-17.5h190q34 0 63 18t45 48l28 57q8 17 23.5 27t34.5 10q28 0 48-18.5t21-46.5q0 1-2-19l-84-335q-7-27-28-44t-49-17H285q-28 0-49.5 17T208-659l-84 335q-2 6-2 18 0 28 20.5 47t49.5 19Zm376.5-291.5Q580-543 580-560t-11.5-28.5Q557-600 540-600t-28.5 11.5Q500-577 500-560t11.5 28.5Q523-520 540-520t28.5-11.5Zm80-80Q660-623 660-640t-11.5-28.5Q637-680 620-680t-28.5 11.5Q580-657 580-640t11.5 28.5Q603-600 620-600t28.5-11.5Zm0 160Q660-463 660-480t-11.5-28.5Q637-520 620-520t-28.5 11.5Q580-497 580-480t11.5 28.5Q603-440 620-440t28.5-11.5Zm80-80Q740-543 740-560t-11.5-28.5Q717-600 700-600t-28.5 11.5Q660-577 660-560t11.5 28.5Q683-520 700-520t28.5-11.5Zm-367 63Q370-477 370-490v-40h40q13 0 21.5-8.5T440-560q0-13-8.5-21.5T410-590h-40v-40q0-13-8.5-21.5T340-660q-13 0-21.5 8.5T310-630v40h-40q-13 0-21.5 8.5T240-560q0 13 8.5 21.5T270-530h40v40q0 13 8.5 21.5T340-460q13 0 21.5-8.5ZM480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-200h120v-240h240v240h120v-360L480-740 240-560v360Zm-80 80v-480l320-240 320 240v480H520v-240h-80v240H160Zm320-350Z"/></svg>

After

Width:  |  Height:  |  Size: 243 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H260Zm0-80h480q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm220-240Z"/></svg>

After

Width:  |  Height:  |  Size: 424 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>

After

Width:  |  Height:  |  Size: 771 B

+236 -70
View File
@@ -54,6 +54,14 @@ enum vi_registers {
VI_REGS_COUNT
};
enum user_event_codes {
USER_EVENT_SAVE_STATE = 1,
USER_EVENT_LOAD_STATE = 2,
USER_EVENT_EXIT_GAME = 3,
USER_EVENT_FAST_FORWARD = 4,
USER_EVENT_LOAD_REWIND = 5,
};
typedef struct {
uint32_t depthbuffer_address;
uint32_t framebuffer_address;
@@ -75,6 +83,9 @@ typedef struct {
FrameBufferInfo frame_buffer_info;
} RDP_DEVICE;
static void *g_tmem = nullptr;
static void *g_hidden_rdram = nullptr;
typedef struct {
uint32_t fps;
uint32_t vis;
@@ -82,6 +93,7 @@ typedef struct {
typedef struct {
std::string message;
uint64_t milliseconds;
Vulkan::ImageHandle image;
} Message;
@@ -97,12 +109,11 @@ static GFX_INFO gfx_info;
static const uint32_t *fragment_spirv;
static size_t fragment_size;
std::vector<bool> rdram_dirty;
uint64_t sync_signal;
static std::vector<bool> rdram_dirty;
static uint64_t sync_signal;
static TTF_Font *message_font;
static std::queue<Message> messages;
static uint64_t message_timer;
static TTF_Font *achievement_challenge_indicator_font;
static std::vector<const char *> achievement_challenge_indicators;
@@ -114,7 +125,7 @@ static bool display_challenge_indicator;
static bool display_fps;
static Vulkan::ImageHandle fps_image;
#define MESSAGE_TIME 3000 // 3 seconds
static std::queue<JoystickEvent> joystick_events;
typedef struct {
float SourceSize[4];
@@ -127,6 +138,12 @@ static const unsigned cmd_len_lut[64] = {
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
};
static void add_joystick_event(void *userdata) {
JoystickEvent *joystick_event = (JoystickEvent *)userdata;
joystick_events.push(*joystick_event);
delete joystick_event;
}
bool sdl_event_filter(void *userdata, SDL_Event *event) {
if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
callback.paused = false;
@@ -134,6 +151,7 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
} else if (event->type == SDL_EVENT_WINDOW_RESIZED && callback.emu_running) {
wsi_platform->do_resize();
} else if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat) {
SDL_Event user_event;
switch (event->key.scancode) {
case SDL_SCANCODE_RETURN:
if (event->key.mod & SDL_KMOD_ALT) {
@@ -143,7 +161,10 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
break;
case SDL_SCANCODE_F:
if (event->key.mod & SDL_KMOD_ALT) {
callback.enable_speedlimiter = !callback.enable_speedlimiter;
SDL_zero(user_event);
user_event.type = SDL_EVENT_USER;
user_event.user.code = USER_EVENT_FAST_FORWARD;
SDL_PushEvent(&user_event);
}
break;
case SDL_SCANCODE_P:
@@ -151,9 +172,14 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
callback.paused = !callback.paused;
}
break;
case SDL_SCANCODE_AC_BACK:
case SDL_SCANCODE_ESCAPE:
if (gfx_info.fullscreen)
callback.emu_running = false;
if (gfx_info.fullscreen) {
SDL_zero(user_event);
user_event.type = SDL_EVENT_USER;
user_event.user.code = USER_EVENT_EXIT_GAME;
SDL_PushEvent(&user_event);
}
break;
case SDL_SCANCODE_F1:
display_fps = !display_fps;
@@ -162,14 +188,26 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
crop_letterbox = !crop_letterbox;
break;
case SDL_SCANCODE_F5:
callback.save_state = true;
SDL_zero(user_event);
user_event.type = SDL_EVENT_USER;
user_event.user.code = USER_EVENT_SAVE_STATE;
SDL_PushEvent(&user_event);
break;
case SDL_SCANCODE_F6:
SDL_zero(user_event);
user_event.type = SDL_EVENT_USER;
user_event.user.code = USER_EVENT_LOAD_REWIND;
SDL_PushEvent(&user_event);
break;
case SDL_SCANCODE_F7:
callback.load_state = true;
SDL_zero(user_event);
user_event.type = SDL_EVENT_USER;
user_event.user.code = USER_EVENT_LOAD_STATE;
SDL_PushEvent(&user_event);
break;
case SDL_SCANCODE_F8:
if (messages.empty())
SDL_RunOnMainThread(ra_display_inprogress_achievements, nullptr, true);
SDL_RunOnMainThread(ra_display_inprogress_achievements, nullptr, false);
break;
case SDL_SCANCODE_F9:
display_challenge_indicator = !display_challenge_indicator;
@@ -177,16 +215,24 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
std::format("Challenge indicators: {}",
display_challenge_indicator ? "ON" : "OFF")
.c_str(),
false);
MESSAGE_VERY_SHORT);
break;
case SDL_SCANCODE_F12:
callback.reset_game = true;
break;
case SDL_SCANCODE_LEFTBRACKET:
callback.lower_volume = true;
if (event->key.mod & SDL_KMOD_ALT) {
callback.decrease_input_delay = true;
} else {
callback.lower_volume = true;
}
break;
case SDL_SCANCODE_RIGHTBRACKET:
callback.raise_volume = true;
if (event->key.mod & SDL_KMOD_ALT) {
callback.increase_input_delay = true;
} else {
callback.raise_volume = true;
}
break;
case SDL_SCANCODE_SLASH:
callback.frame_advance = true;
@@ -211,21 +257,65 @@ bool sdl_event_filter(void *userdata, SDL_Event *event) {
default:
break;
}
} else if (event->type == SDL_EVENT_USER) {
switch (event->user.code) {
case USER_EVENT_SAVE_STATE:
callback.save_state = true;
break;
case USER_EVENT_LOAD_REWIND:
callback.load_rewind = true;
break;
case USER_EVENT_LOAD_STATE:
callback.load_state = true;
break;
case USER_EVENT_EXIT_GAME:
callback.emu_running = false;
break;
case USER_EVENT_FAST_FORWARD:
callback.enable_speedlimiter = !callback.enable_speedlimiter;
break;
default:
break;
}
} else if (event->type == SDL_EVENT_JOYSTICK_ADDED) {
JoystickEvent *joystick_event = new JoystickEvent;
joystick_event->joystick_id = event->jdevice.which;
joystick_event->connected = true;
SDL_RunOnMainThread(add_joystick_event, joystick_event, false);
} else if (event->type == SDL_EVENT_JOYSTICK_REMOVED) {
JoystickEvent *joystick_event = new JoystickEvent;
joystick_event->joystick_id = event->jdevice.which;
joystick_event->connected = false;
SDL_RunOnMainThread(add_joystick_event, joystick_event, false);
} else if (event->type == SDL_EVENT_WILL_ENTER_BACKGROUND) {
wsi->end_frame();
wsi->deinit_surface_and_swapchain();
} else if (event->type == SDL_EVENT_RENDER_DEVICE_RESET) {
wsi->init_surface_swapchain();
wsi->begin_frame();
}
return 0;
}
void rdp_new_processor(GFX_INFO _gfx_info) {
gfx_info = _gfx_info;
JoystickEvent get_joystick_event() {
if (joystick_events.empty())
return JoystickEvent{0, false};
JoystickEvent joystick_event = joystick_events.front();
joystick_events.pop();
return joystick_event;
}
void rdp_idle() {
processor->idle();
sync_signal = 0;
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
}
if (processor) {
delete processor;
}
RDP::CommandProcessorFlags flags = 0;
static void rdp_new_processor() {
RDP::CommandProcessorFlags flags =
RDP::COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_HIDDEN_RDRAM_BIT |
RDP::COMMAND_PROCESSOR_FLAG_HOST_VISIBLE_TMEM_BIT;
if (gfx_info.upscale == 2) {
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
@@ -236,11 +326,24 @@ void rdp_new_processor(GFX_INFO _gfx_info) {
} else if (gfx_info.upscale == 8) {
flags |= RDP::COMMAND_PROCESSOR_FLAG_SUPER_SAMPLED_DITHER_BIT;
flags |= RDP::COMMAND_PROCESSOR_FLAG_UPSCALING_8X_BIT;
} else {
gfx_info.upscale = 1;
}
processor = new RDP::CommandProcessor(wsi->get_device(), gfx_info.RDRAM, 0,
gfx_info.RDRAM_SIZE,
gfx_info.RDRAM_SIZE / 2, flags);
g_tmem = processor->get_tmem();
if (!g_tmem) {
LOGE("Failed to get tmem\n");
}
g_hidden_rdram = processor->begin_read_hidden_rdram();
if (!g_hidden_rdram) {
LOGE("Failed to get hidden_rdram\n");
}
sync_signal = 0;
rdram_dirty.assign(gfx_info.RDRAM_SIZE >> 3, false);
}
static ImageHandle create_message_image(Vulkan::Device &device, int width,
@@ -267,12 +370,13 @@ static ImageHandle create_message_image(Vulkan::Device &device, int width,
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
size_t font_size, uint32_t save_state_slot) {
memset(&rdp_device, 0, sizeof(RDP_DEVICE));
memset(&callback, 0, sizeof(CALL_BACK));
window = (SDL_Window *)_window;
SDL_SyncWindow(window);
bool result = SDL_AddEventWatch(sdl_event_filter, nullptr);
if (!result) {
printf("Could not add event watch.\n");
LOGE("Could not add event watch.\n");
return;
}
@@ -309,7 +413,7 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
return;
}
rdp_new_processor(gfx_info);
rdp_new_processor();
if (!processor->device_is_supported()) {
rdp_close();
@@ -334,7 +438,6 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
crop_letterbox = false;
messages = std::queue<Message>();
message_timer = 0;
display_challenge_indicator = true;
achievement_challenge_indicators.clear();
@@ -346,6 +449,11 @@ void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
}
void rdp_close() {
display_fps = false;
g_tmem = nullptr;
g_hidden_rdram = nullptr;
messages = std::queue<Message>();
achievement_challenge_indicator_image = Vulkan::ImageHandle();
achievement_progress_indicator_image = Vulkan::ImageHandle();
@@ -374,6 +482,8 @@ void rdp_close() {
delete wsi_platform;
wsi_platform = nullptr;
}
SDL_RemoveEventWatch(sdl_event_filter, nullptr);
}
static void calculate_viewport(float *x, float *y, float *width, float *height,
@@ -444,11 +554,34 @@ static void draw_fps(CommandBufferHandle cmd, VkViewport vp) {
cmd->draw(3);
}
static void pop_message(void *userdata) { messages.pop(); }
static uint32_t pop_message_callback(void *userdata, SDL_TimerID timerID,
uint32_t interval) {
SDL_RunOnMainThread(pop_message, NULL, false);
return 0;
}
static void render_frame(Vulkan::Device &device) {
RDP::ScanoutOptions options = {};
options.persist_frame_on_invalid_input = true;
options.blend_previous_frame = true;
options.upscale_deinterlacing = false;
options.blend_previous_frame = !gfx_info.ssaa;
options.upscale_deinterlacing = gfx_info.ssaa;
if (gfx_info.ssaa) {
switch (gfx_info.upscale) {
case 2:
options.downscale_steps = 1;
break;
case 4:
options.downscale_steps = 2;
break;
case 8:
options.downscale_steps = 3;
break;
default:
options.downscale_steps = 0;
break;
}
}
if (crop_letterbox && gfx_info.widescreen) {
options.crop_rect.enable = true;
@@ -489,10 +622,10 @@ static void render_frame(Vulkan::Device &device) {
cmd->set_depth_test(false, false);
cmd->set_cull_mode(VK_CULL_MODE_NONE);
VkViewport vp = cmd->get_viewport();
// If we don't have an image, we just get a cleared screen in the render
// pass.
if (image) {
VkViewport vp = cmd->get_viewport();
calculate_viewport(&vp.x, &vp.y, &vp.width, &vp.height,
image->get_height() / gfx_info.upscale);
@@ -513,37 +646,32 @@ static void render_frame(Vulkan::Device &device) {
// The vertices are constants in the shader.
// Draws fullscreen quad using oversized triangle.
cmd->draw(3);
if (!messages.empty()) {
Message *message = &messages.front();
if (!message->image) {
message->image = create_message_image(device, vp.width, message_font,
message->message.c_str());
}
cmd->set_texture(0, 0, message->image->get_view(),
Vulkan::StockSampler::NearestClamp);
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
vp.y = vp.y + vp.height - message->image->get_height();
vp.height = message->image->get_height();
vp.width = message->image->get_width();
cmd->set_viewport(vp);
cmd->draw(3);
if (SDL_GetTicks() > message_timer) {
messages.pop();
message_timer = SDL_GetTicks() + MESSAGE_TIME;
}
} else if (achievement_progress_indicator_image) {
draw_indicator(cmd, achievement_progress_indicator_image, vp);
} else if (achievement_challenge_indicator_image &&
display_challenge_indicator) {
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
}
if (!messages.empty()) {
Message *message = &messages.front();
if (!message->image) {
message->image = create_message_image(device, vp.width, message_font,
message->message.c_str());
SDL_AddTimer(message->milliseconds, pop_message_callback, NULL);
}
cmd->set_texture(0, 0, message->image->get_view(),
Vulkan::StockSampler::NearestClamp);
vp.x = floor(vp.x + (vp.width - message->image->get_width()) / 2);
vp.y = vp.y + vp.height - message->image->get_height();
vp.height = message->image->get_height();
vp.width = message->image->get_width();
cmd->set_viewport(vp);
if (messages.empty() && display_fps && fps_image) {
draw_fps(cmd, vp);
}
cmd->draw(3);
} else if (achievement_progress_indicator_image) {
draw_indicator(cmd, achievement_progress_indicator_image, vp);
} else if (achievement_challenge_indicator_image &&
display_challenge_indicator) {
draw_indicator(cmd, achievement_challenge_indicator_image, vp);
}
if (messages.empty() && display_fps && fps_image) {
draw_fps(cmd, vp);
}
cmd->end_render_pass();
@@ -561,17 +689,26 @@ void rdp_render_frame() {
}
void rdp_update_screen() {
wsi->end_frame();
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
return;
}
if (!wsi->end_frame()) {
LOGE("End frame failed\n");
SDL_PumpEvents(); // For Android to trigger pause event
}
wsi->begin_frame();
}
CALL_BACK rdp_check_callback() {
CALL_BACK return_value = callback;
callback.save_state = false;
callback.load_rewind = false;
callback.load_state = false;
callback.reset_game = false;
callback.lower_volume = false;
callback.raise_volume = false;
callback.decrease_input_delay = false;
callback.increase_input_delay = false;
callback.frame_advance = false;
return return_value;
}
@@ -597,27 +734,49 @@ void rdp_check_framebuffers(uint32_t address, uint32_t length) {
}
}
size_t rdp_state_size() { return sizeof(RDP_DEVICE); }
size_t rdp_state_size() {
return sizeof(RDP_DEVICE) + 0x1000 + processor->get_hidden_rdram_size();
}
void rdp_save_state(uint8_t *state) {
processor->wait_for_timeline(processor->signal_timeline());
memcpy(state, &rdp_device, sizeof(RDP_DEVICE));
if (g_tmem) {
memcpy(state + sizeof(RDP_DEVICE), g_tmem, 0x1000);
}
if (g_hidden_rdram) {
memcpy(state + sizeof(RDP_DEVICE) + 0x1000, g_hidden_rdram,
processor->get_hidden_rdram_size());
}
}
void rdp_load_state(const uint8_t *state) {
void rdp_load_state(GFX_INFO _gfx_info, const uint8_t *state) {
gfx_info = _gfx_info;
memcpy(&rdp_device, state, sizeof(RDP_DEVICE));
if (g_tmem) {
memcpy(g_tmem, state + sizeof(RDP_DEVICE), 0x1000);
}
if (g_hidden_rdram) {
memcpy(g_hidden_rdram, state + sizeof(RDP_DEVICE) + 0x1000,
processor->get_hidden_rdram_size());
}
}
static void push_onscreen_message(void *message) {
if (messages.empty())
message_timer = SDL_GetTicks() + MESSAGE_TIME;
messages.push({std::string((const char *)message), Vulkan::ImageHandle()});
static void push_onscreen_message(void *data) {
Message *message = (Message *)data;
messages.push(*message);
delete message;
}
void rdp_onscreen_message(const char *message, bool long_message) {
SDL_RunOnMainThread(push_onscreen_message, (void *)message, true);
if (long_message)
SDL_RunOnMainThread(push_onscreen_message, (void *)message, true);
void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds) {
Message *data = new Message;
data->message = message;
data->milliseconds = static_cast<uint64_t>(milliseconds);
data->image = Vulkan::ImageHandle();
SDL_RunOnMainThread(push_onscreen_message, data, false);
}
uint32_t pixel_size(uint32_t pixel_type, uint32_t area) {
@@ -631,7 +790,7 @@ uint32_t pixel_size(uint32_t pixel_type, uint32_t area) {
case 3:
return area * 4;
default:
printf("Invalid pixel size: %u\n", pixel_type);
LOGE("Invalid pixel size: %u\n", pixel_type);
return 0;
}
}
@@ -849,17 +1008,24 @@ uint64_t rdp_process_commands() {
return interrupt_timer;
}
void rdp_set_fps_callback(void *userdata) {
static void rdp_set_fps_callback(void *userdata) {
FPS_DATA *data = (FPS_DATA *)userdata;
if (!wsi) {
delete data;
return;
}
fps_image = create_message_image(
wsi->get_device(), 0, achievement_challenge_indicator_font,
std::format("FPS: {} VI/S: {}", data->fps, data->vis).c_str());
delete data;
}
void rdp_set_fps(uint32_t fps, uint32_t vis) {
if (display_fps) {
FPS_DATA data = {fps, vis};
SDL_RunOnMainThread(rdp_set_fps_callback, &data, true);
FPS_DATA *data = new FPS_DATA;
data->fps = fps;
data->vis = vis;
SDL_RunOnMainThread(rdp_set_fps_callback, data, false);
}
}
+20 -3
View File
@@ -21,6 +21,7 @@ typedef struct {
bool vsync;
bool integer_scaling;
uint32_t upscale;
bool ssaa;
bool crt;
} GFX_INFO;
@@ -28,15 +29,29 @@ typedef struct {
bool emu_running;
bool save_state;
bool load_state;
bool load_rewind;
bool reset_game;
bool enable_speedlimiter;
bool lower_volume;
bool raise_volume;
bool decrease_input_delay;
bool increase_input_delay;
bool paused;
bool frame_advance;
uint32_t save_state_slot;
} CALL_BACK;
typedef struct {
uint32_t joystick_id;
bool connected;
} JoystickEvent;
typedef enum {
MESSAGE_VERY_SHORT = 500,
MESSAGE_SHORT = 3000,
MESSAGE_LONG = 6000,
} MESSAGE_LENGTH;
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
size_t font_size, uint32_t save_state_slot);
void rdp_close();
@@ -45,14 +60,16 @@ void rdp_update_screen();
void rdp_render_frame();
CALL_BACK rdp_check_callback();
uint64_t rdp_process_commands();
void rdp_onscreen_message(const char *message, bool long_message);
void rdp_new_processor(GFX_INFO _gfx_info);
void rdp_idle();
void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds);
void rdp_check_framebuffers(uint32_t address, uint32_t length);
size_t rdp_state_size();
void rdp_save_state(uint8_t *state);
void rdp_load_state(const uint8_t *state);
void rdp_load_state(GFX_INFO _gfx_info, const uint8_t *state);
void rdp_set_fps(uint32_t fps, uint32_t vis);
JoystickEvent get_joystick_event();
void achievement_challenge_indicator_add(const char *achievement_title);
void achievement_challenge_indicator_remove(const char *achievement_title);
void achievement_progress_add(const char *achievement_title,
+1 -1
View File
@@ -46,7 +46,7 @@ uint32_t SDL_WSIPlatform::get_surface_height() {
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi) { return true; }
void SDL_WSIPlatform::poll_input() {}
void SDL_WSIPlatform::poll_input() { SDL_PumpEvents(); }
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler) {}
+47 -25
View File
@@ -16,18 +16,20 @@ void store_retroachievements_credentials(const char *username,
void notify_load_game(void *userdata);
static rc_client_t *g_client = NULL;
static const uint8_t *g_dmem = NULL;
static size_t g_dmem_size = 0;
static const uint8_t *g_rdram = NULL;
static size_t g_rdram_size = 0;
static bool g_challenge = false;
static bool g_leaderboard = false;
static rc_client_leaderboard_list_t *g_leaderboard_list = NULL;
static char g_rich_presence_buffer[512];
static uint32_t read_memory(uint32_t address, uint8_t *buffer,
uint32_t num_bytes, rc_client_t *client) {
if (address + num_bytes > g_dmem_size)
if (address + num_bytes > g_rdram_size)
memset(buffer, 0, num_bytes);
else
memcpy(buffer, &g_dmem[address], num_bytes);
memcpy(buffer, &g_rdram[address], num_bytes);
return num_bytes;
}
@@ -84,10 +86,6 @@ const char *ra_get_username() {
const rc_client_user_t *user = rc_client_get_user_info(g_client);
return user ? user->username : NULL;
}
const char *ra_get_token() {
const rc_client_user_t *user = rc_client_get_user_info(g_client);
return user ? user->token : NULL;
}
void ra_logout_user() { rc_client_logout(g_client); }
@@ -128,29 +126,40 @@ static void load_game_callback(int result, const char *error_message,
notify_load_game(userdata);
}
void ra_welcome() {
if (!g_client)
void ra_welcome(const char **game_title, const char **game_image_url) {
if (!g_client) {
*game_title = NULL;
*game_image_url = NULL;
return;
}
char buffer[512];
char *error_message = rc_client_get_userdata(g_client);
if (error_message) {
snprintf(buffer, sizeof(buffer), "RA Error: %s", error_message);
rdp_onscreen_message(buffer, true);
rdp_onscreen_message(buffer, MESSAGE_LONG);
free(error_message);
rc_client_set_userdata(g_client, NULL);
*game_title = NULL;
*game_image_url = NULL;
return;
}
const rc_client_game_t *game = rc_client_get_game_info(g_client);
if (!game) {
*game_title = NULL;
*game_image_url = NULL;
return;
}
rc_client_user_game_summary_t summary;
rc_client_get_user_game_summary(g_client, &summary);
int message_length = snprintf(
buffer, sizeof(buffer), "%s\nMode: %s\n", game->title,
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Softcore");
rc_client_get_hardcore_enabled(g_client) ? "Hardcore" : "Casual");
if (summary.num_core_achievements != 0) {
snprintf(buffer + message_length, sizeof(buffer) - message_length,
@@ -160,7 +169,10 @@ void ra_welcome() {
snprintf(buffer + message_length, sizeof(buffer) - message_length,
"Game has no achievements");
}
rdp_onscreen_message(buffer, true);
rdp_onscreen_message(buffer, MESSAGE_LONG);
*game_title = game->title;
*game_image_url = game->badge_url;
}
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata) {
@@ -177,9 +189,9 @@ void ra_unload_game() {
rc_client_unload_game(g_client);
}
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size) {
g_dmem = dmem;
g_dmem_size = dmem_size;
void ra_set_rdram(const uint8_t *rdram, size_t rdram_size) {
g_rdram = rdram;
g_rdram_size = rdram_size;
}
static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
@@ -187,14 +199,14 @@ static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
snprintf(buffer, sizeof(buffer), "Leaderboard submitted: %s - %s",
leaderboard->title, leaderboard->tracker_value);
rdp_onscreen_message(buffer, false);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void achievement_triggered(const rc_client_achievement_t *achievement) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "Unlocked: %s", achievement->title);
rdp_onscreen_message(buffer, false);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void game_completed(rc_client_t *client) {
@@ -204,7 +216,7 @@ static void game_completed(rc_client_t *client) {
snprintf(buffer, sizeof(buffer), "%s: %s",
rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",
game->title);
rdp_onscreen_message(buffer, false);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void subset_completed(const rc_client_subset_t *subset,
@@ -214,7 +226,7 @@ static void subset_completed(const rc_client_subset_t *subset,
snprintf(buffer, sizeof(buffer), "Subset %s: %s",
rc_client_get_hardcore_enabled(client) ? "mastered" : "completed",
subset->title);
rdp_onscreen_message(buffer, false);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static void server_error(const rc_client_server_error_t *server_error) {
@@ -222,7 +234,7 @@ static void server_error(const rc_client_server_error_t *server_error) {
snprintf(buffer, sizeof(buffer), "RA server error: %s",
server_error->error_message);
rdp_onscreen_message(buffer, false);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
static const char *get_leaderboard_title(const char *display) {
@@ -315,9 +327,10 @@ static void event_handler(const rc_client_event_t *event, rc_client_t *client) {
void ra_init_client(bool hardcore, bool challenge, bool leaderboard) {
// Create the client instance (using a global variable simplifies this
// example)
g_client = rc_client_create(read_memory, server_call);
g_dmem = NULL;
g_dmem_size = 0;
if (!g_client)
g_client = rc_client_create(read_memory, server_call);
g_rdram = NULL;
g_rdram_size = 0;
// Provide a logging function to simplify debugging
rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_WARN, log_message);
@@ -344,6 +357,15 @@ void ra_do_frame() { rc_client_do_frame(g_client); }
void ra_do_idle() { rc_client_idle(g_client); }
const char *ra_get_rich_presence() {
size_t size = rc_client_get_rich_presence_message(
g_client, g_rich_presence_buffer, sizeof(g_rich_presence_buffer));
if (size == 0) {
return NULL;
}
return g_rich_presence_buffer;
}
size_t ra_state_size() { return rc_client_progress_size(g_client); }
void ra_save_state(uint8_t *state, size_t state_size) {
@@ -393,7 +415,7 @@ void ra_display_inprogress_achievements(void *userdata) {
}
}
if (buffer_length > 0) {
rdp_onscreen_message(buffer, false);
rdp_onscreen_message(buffer, MESSAGE_SHORT);
}
rc_client_destroy_achievement_list(list);
}
+4 -3
View File
@@ -4,11 +4,11 @@
void ra_init_client(bool hardcore, bool challenge, bool leaderboard);
void ra_shutdown_client();
void ra_welcome();
void ra_welcome(const char **game_title, const char **game_image_url);
bool ra_get_hardcore();
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata);
void ra_unload_game();
void ra_set_dmem(const uint8_t *dmem, size_t dmem_size);
void ra_set_rdram(const uint8_t *rdram, size_t rdram_size);
void ra_do_frame();
void ra_do_idle();
void ra_http_callback(const char *content, size_t content_size, int status_code,
@@ -18,11 +18,12 @@ void ra_login_user(const char *username, const char *password, void *userdata);
void ra_login_token_user(const char *username, const char *token,
void *userdata);
const char *ra_get_username();
const char *ra_get_token();
size_t ra_state_size();
void ra_save_state(uint8_t *state, size_t state_size);
void ra_load_state(const uint8_t *state, size_t state_size);
const char *ra_get_rich_presence();
#ifdef __cplusplus
extern "C" {
#endif
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.95.0"
channel = "1.97.0"
+18 -11
View File
@@ -8,7 +8,7 @@ pub struct DecodedCheat {
data: u16,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cheats {
pub cheats: Vec<Vec<DecodedCheat>>,
pub boot: bool,
@@ -26,7 +26,7 @@ pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<S
pub fn init(
device: &mut device::Device,
cheat_settings: std::collections::HashMap<String, Option<String>>,
cheat_settings: rustc_hash::FxHashMap<String, Option<String>>,
) {
let cheats = serde_json::from_slice::<Cheat>(include_bytes!("../data/cheats.json"))
.unwrap()
@@ -48,7 +48,8 @@ pub fn init(
{
option_value = Some(found_option_value.to_string());
} else {
panic!("Cheat option: {option} not found");
eprintln!("Cheat option: {option} not found");
continue;
}
}
@@ -65,21 +66,27 @@ pub fn init(
}
let mut split = result.split_whitespace();
let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
decoded_cheat.push(DecodedCheat {
code_type: (first_part >> 24) as u8,
address: first_part & 0x00FFFFFF,
data: u16::from_str_radix(split.next().unwrap(), 16).unwrap(),
});
if let Ok(data) = u16::from_str_radix(split.next().unwrap(), 16) {
decoded_cheat.push(DecodedCheat {
code_type: (first_part >> 24) as u8,
address: first_part & 0x00FFFFFF,
data,
})
} else {
eprintln!("Could not parse data for: {}", cheat_setting.0);
};
}
if !decoded_cheat.is_empty() {
device.cheats.cheats.push(decoded_cheat);
}
device.cheats.cheats.push(decoded_cheat);
} else {
println!("Could not find cheat: {}", cheat_setting.0);
eprintln!("Could not find cheat: {}", cheat_setting.0);
}
}
if !device.cheats.cheats.is_empty() {
device.cheats.enabled = true;
ui::video::onscreen_message("Cheats enabled", false);
ui::video::onscreen_message("Cheats enabled", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
}
}
+157 -71
View File
@@ -10,10 +10,13 @@ include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs"));
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
const N64_EXTENSIONS_UNCOMPRESSED: [&str; 8] =
["n64", "v64", "z64", "bin", "N64", "V64", "Z64", "BIN"];
use rand::{Rng, SeedableRng};
use crate::{cheats, netplay, retroachievements, ui};
use std::{collections::HashMap, fs, io::Read};
use crate::{cheats, netplay, retroachievements, savestates, ui};
use std::io::Read;
pub mod ai;
pub mod cache;
@@ -44,29 +47,54 @@ pub mod tlb;
pub mod unmapped;
pub mod vi;
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::GameSettings) {
pub fn run_game(
device: &mut Device,
rom_contents: &[u8],
game_settings: ui::GameSettings,
ra_config: retroachievements::RAConfig,
netplay_config: Option<netplay::NetplayConfig>,
) {
device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak {
device.rdram.size = 0x400000;
}
if let Some(slot) = game_settings.load_savestate_slot {
device.ui.storage.save_state_slot = slot;
device.load_state = true;
device.savestate.load_state = true;
}
init_rng(device);
cart::rom::init(device, rom_contents); // cart needs to come before rdram
// rdram pointer is shared with parallel-rdp and retroachievements
rdram::init(device);
ui::video::init(device);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::video::init(device, netplay_config.is_some());
ui::audio::init(device);
ui::input::init(&mut device.ui);
// must be after video init
retroachievements::welcome();
if let Some(netplay_config) = &netplay_config {
device.netplay = netplay::init(
device,
netplay_config,
cart::rom::is_system_pal(rom_contents),
);
if device.netplay.is_none() {
ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui);
ui::video::close(&device.ui);
return;
}
}
init_rng_rtc(device);
// must be after video init
let (discord_watch_tx, discord_handle) = if ra_config.enabled && device.netplay.is_none() {
retroachievements::load_game(rom_contents, rom_contents.len(), ra_config.rich_presence)
} else {
(None, None)
};
mi::init(device);
pif::init(device);
@@ -82,6 +110,7 @@ pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::Gam
ui::storage::init(&mut device.ui, &device.cart.rom);
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
ui::storage::format_saves(device);
if !retroachievements::get_hardcore() {
cheats::init(device, game_settings.cheats);
@@ -89,9 +118,14 @@ pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::Gam
cpu::run(device);
retroachievements::unload_game();
retroachievements::unload_game(discord_watch_tx, discord_handle);
ui::storage::write_saves(device);
if let Some(netplay) = &mut device.netplay {
netplay::close(netplay);
}
ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui);
ui::video::close(&device.ui);
@@ -101,7 +135,7 @@ fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
}
fn init_rng(device: &mut Device) {
fn init_rng_rtc(device: &mut Device) {
let mut rng_seed = set_rng().next_u64();
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
@@ -111,6 +145,18 @@ fn init_rng(device: &mut Device) {
}
}
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
device.cart.rtc_timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
} else {
device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
}
}
}
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
@@ -139,24 +185,23 @@ fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
}
}
pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
pub fn get_rom_contents(file_path: &std::path::PathBuf) -> Option<Vec<u8>> {
let mut contents = vec![];
if file_path
.extension()
.unwrap_or_default()
.eq_ignore_ascii_case("zip")
{
let zip_file = fs::File::open(file_path).unwrap();
#[cfg(target_os = "android")]
let zip_file = ui::android::get_file_from_uri(file_path).unwrap();
#[cfg(not(target_os = "android"))]
let zip_file = std::fs::File::open(file_path).unwrap();
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
for i in 0..archive.len() {
let mut file = archive.by_index(i).unwrap();
let extension = file
.enclosed_name()
.unwrap()
.extension()
.unwrap()
.to_ascii_lowercase();
if extension == "z64" || extension == "n64" || extension == "v64" {
if let Some(extension) = file.enclosed_name().unwrap().extension()
&& N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
{
file.read_to_end(&mut contents)
.expect("could not read zip file");
break;
@@ -167,16 +212,20 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
.unwrap_or_default()
.eq_ignore_ascii_case("7z")
{
#[cfg(target_os = "android")]
let sevenz_file = ui::android::get_file_from_uri(file_path).unwrap();
#[cfg(not(target_os = "android"))]
let sevenz_file = std::fs::File::open(file_path).unwrap();
let mut archive =
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
sevenz_rust2::ArchiveReader::new(sevenz_file, sevenz_rust2::Password::empty()).unwrap();
let mut found = false;
archive
.for_each_entries(
&mut |entry: &sevenz_rust2::ArchiveEntry, reader: &mut dyn std::io::Read| {
let name = entry.name().to_ascii_lowercase();
if !found
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
&& let Some(extension) = std::path::PathBuf::from(entry.name()).extension()
&& N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
{
reader
.read_to_end(&mut contents)
@@ -191,8 +240,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
)
.expect("ok");
} else {
contents = fs::read(file_path)
.unwrap_or_else(|_| panic!("Could not read ROM file: {}", file_path.display()));
#[cfg(target_os = "android")]
let mut file = ui::android::get_file_from_uri(file_path).unwrap();
#[cfg(not(target_os = "android"))]
let mut file = std::fs::File::open(file_path).unwrap();
file.read_to_end(&mut contents).unwrap();
}
if contents.is_empty() {
@@ -202,15 +254,28 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SpeedLimiter {
pub limit_freq: u64,
#[serde(skip)]
#[serde(default = "std::time::Instant::now")]
pub limit_freq_check: std::time::Instant,
pub frame_counter: u64,
pub enabled: bool,
pub min_wait_time: std::time::Duration,
#[serde(skip)]
pub next_pace_deadline: Option<std::time::Instant>,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Device {
#[serde(skip)]
pub netplay: Option<netplay::Netplay>,
#[serde(skip, default = "ui::Ui::default")]
#[serde(skip)]
pub ui: ui::Ui,
pub speed_limiter: SpeedLimiter,
pub byte_swap: usize,
pub save_state: bool,
pub load_state: bool,
pub savestate: savestates::Savestate,
pub cpu: cpu::Cpu,
pub pif: pif::Pif,
pub cart: cart::Cart,
@@ -224,7 +289,6 @@ pub struct Device {
pub ai: ai::Ai,
pub si: si::Si,
pub ri: ri::Ri,
#[serde(skip, default = "set_rng")]
pub rng: rand::rngs::Xoshiro256PlusPlus,
pub vru: controller::vru::Vru,
pub transferpaks: [controller::transferpak::TransferPak; 4],
@@ -236,7 +300,27 @@ pub fn zero_m128i() -> __m128i {
}
impl Device {
pub fn new() -> Device {
pub fn clone_state(&mut self, device: &Device) {
self.rng.clone_from(&device.rng);
self.cpu.clone_from(&device.cpu);
self.pif.clone_from(&device.pif);
self.cart.clone_from(&device.cart);
self.memory.clone_from(&device.memory);
self.rsp.clone_from(&device.rsp);
self.rdp.clone_from(&device.rdp);
self.rdram.clone_from(&device.rdram);
self.mi.clone_from(&device.mi);
self.pi.clone_from(&device.pi);
self.vi.clone_from(&device.vi);
self.ai.clone_from(&device.ai);
self.si.clone_from(&device.si);
self.ri.clone_from(&device.ri);
self.vru.clone_from(&device.vru);
self.transferpaks.clone_from(&device.transferpaks);
self.cheats.clone_from(&device.cheats);
}
pub fn new(with_ui: bool) -> Box<Device> {
let mut byte_swap: usize = 0;
let test: [u8; 4] = [1, 2, 3, 4];
// if the host computer is little endian, that means the RDRAM will be stored as little endian
@@ -244,34 +328,41 @@ impl Device {
if u32::from_le_bytes(test) == u32::from_ne_bytes(test) {
byte_swap = 3;
}
Device {
Box::new(Device {
netplay: None,
ui: ui::Ui::new(),
ui: if with_ui {
ui::Ui::new()
} else {
ui::Ui::default()
},
speed_limiter: SpeedLimiter {
limit_freq: 2,
limit_freq_check: std::time::Instant::now(),
frame_counter: 0,
enabled: true,
min_wait_time: std::time::Duration::from_secs(1),
next_pace_deadline: None,
},
byte_swap,
save_state: false,
load_state: false,
savestate: savestates::Savestate {
save_state: false,
load_state: false,
load_rewind: false,
last_rewind_saved: 0.0,
rewind_pool: std::sync::Arc::new(tokio::sync::Mutex::new(
std::collections::BTreeMap::new(),
)),
},
cpu: cpu::Cpu {
cop0: cop0::Cop0 {
regs: [0; cop0::COP0_REGS_COUNT as usize],
reg_write_masks: [0; cop0::COP0_REGS_COUNT as usize],
regs: [0; cop0::COP0_REGS_COUNT],
reg_write_masks: [0; cop0::COP0_REGS_COUNT],
reg_latch: 0,
is_event: false,
instrs: [cop0::reserved; 32],
instrs2: [cop0::reserved; 32],
tlb_lut_w: vec![
tlb::TlbLut {
address: 0,
cached: false,
};
0x100000
],
tlb_lut_r: vec![
tlb::TlbLut {
address: 0,
cached: false,
};
0x100000
],
tlb_lut_w: rustc_hash::FxHashMap::default(),
tlb_lut_r: rustc_hash::FxHashMap::default(),
tlb_entries: [tlb::TlbEntry {
mask: 0,
vpn2: 0,
@@ -357,9 +448,9 @@ impl Device {
rtc_timestamp: 0,
rtc: cart::AfRtc { control: 0x0200 },
sc64: cart::sc64::Sc64 {
regs: [0; cart::sc64::SC64_REGS_COUNT as usize],
regs: [0; cart::sc64::SC64_REGS_COUNT],
regs_locked: true,
cfg: [0; cart::sc64::SC64_CFG_COUNT as usize],
cfg: [0; cart::sc64::SC64_CFG_COUNT],
sector: 0,
buffer: vec![0; 8192],
writeback_sector: vec![0; 256],
@@ -395,7 +486,7 @@ impl Device {
rdram: rdram::Rdram {
mem: vec![],
size: 0x800000,
regs: [[0; rdram::RDRAM_REGS_COUNT as usize]; 4],
regs: [[0; rdram::RDRAM_REGS_COUNT]; 4],
},
rsp: rsp_interface::Rsp {
cpu: rsp_cpu::Cpu {
@@ -440,8 +531,8 @@ impl Device {
instrs: [rsp_su_instructions::reserved; 64],
vec_instrs: [rsp_vu_instructions::reserved; 64],
},
regs: [0; rsp_interface::SP_REGS_COUNT as usize],
regs2: [0; rsp_interface::SP_REGS2_COUNT as usize],
regs: [0; rsp_interface::SP_REGS_COUNT],
regs2: [0; rsp_interface::SP_REGS2_COUNT],
mem: [0; 0x2000],
last_status_value: 0,
run_after_dma: false,
@@ -453,19 +544,19 @@ impl Device {
}; 2],
},
rdp: rdp::Rdp {
regs_dpc: [0; rdp::DPC_REGS_COUNT as usize],
regs_dps: [0; rdp::DPS_REGS_COUNT as usize],
regs_dpc: [0; rdp::DPC_REGS_COUNT],
regs_dps: [0; rdp::DPS_REGS_COUNT],
wait_frozen: false,
last_status_value: 0,
},
mi: mi::Mi {
regs: [0; mi::MI_REGS_COUNT as usize],
regs: [0; mi::MI_REGS_COUNT],
},
pi: pi::Pi {
regs: [0; pi::PI_REGS_COUNT as usize],
regs: [0; pi::PI_REGS_COUNT],
},
ai: ai::Ai {
regs: [0; ai::AI_REGS_COUNT as usize],
regs: [0; ai::AI_REGS_COUNT],
last_read: 0,
freq: 33600,
delayed_carry: false,
@@ -476,26 +567,21 @@ impl Device {
}; 2],
},
si: si::Si {
regs: [0; si::SI_REGS_COUNT as usize],
regs: [0; si::SI_REGS_COUNT],
dma_dir: si::DmaDir::None,
},
ri: ri::Ri {
regs: [0; ri::RI_REGS_COUNT as usize],
regs: [0; ri::RI_REGS_COUNT],
ram_init: false,
},
vi: vi::Vi {
regs: [0; vi::VI_REGS_COUNT as usize],
regs: [0; vi::VI_REGS_COUNT],
clock: 0,
field: 0,
delay: 0,
count_per_scanline: 0,
enable_speed_limiter: true,
next_pace_deadline: None,
vi_counter: 0,
min_wait_time: std::time::Duration::from_secs(1),
frame_time: 0.0,
limit_freq: 2,
limit_freq_check: std::time::Instant::now(),
elapsed_time: 0.0,
},
vru: controller::vru::Vru {
status: 0,
@@ -505,7 +591,7 @@ impl Device {
word_buffer: [0; 40],
words: Vec::new(),
talking: false,
word_mappings: HashMap::new(),
word_mappings: rustc_hash::FxHashMap::default(),
},
rng: set_rng(),
transferpaks: [
@@ -519,6 +605,6 @@ impl Device {
boot: true,
enabled: false,
},
}
})
}
}
+27 -26
View File
@@ -1,19 +1,19 @@
use crate::{device, ui};
const AI_DRAM_ADDR_REG: u32 = 0;
const AI_LEN_REG: u32 = 1;
// const AI_CONTROL_REG: u32 = 2;
const AI_STATUS_REG: u32 = 3;
const AI_DACRATE_REG: u32 = 4;
// const AI_BITRATE_REG: u32 = 5;
pub const AI_REGS_COUNT: u32 = 6;
const AI_DRAM_ADDR_REG: usize = 0;
const AI_LEN_REG: usize = 1;
// const AI_CONTROL_REG: usize = 2;
pub const AI_STATUS_REG: usize = 3;
const AI_DACRATE_REG: usize = 4;
// const AI_BITRATE_REG: usize = 5;
pub const AI_REGS_COUNT: usize = 6;
const AI_STATUS_BUSY: u32 = 0x40000000;
pub const AI_STATUS_BUSY: u32 = 0x40000000;
const AI_STATUS_FULL: u32 = 0x80000000;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Ai {
pub regs: [u32; AI_REGS_COUNT as usize],
pub regs: [u32; AI_REGS_COUNT],
pub fifo: [AiDma; 2],
pub last_read: u64,
pub delayed_carry: bool,
@@ -38,7 +38,7 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
}
let remaining_dma_duration =
next_ai_event.unwrap().count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize];
next_ai_event.unwrap().count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG];
let dma_length = remaining_dma_duration * device.ai.fifo[0].length / device.ai.fifo[0].duration;
dma_length & !7
@@ -46,7 +46,7 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
fn get_dma_duration(device: &device::Device) -> u64 {
let bytes_per_sample = 4; /* XXX: assume 16bit stereo - should depends on bitrate instead */
let length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
let length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
(length * device.cpu.clock_rate) / (bytes_per_sample * device.ai.freq)
}
@@ -73,34 +73,36 @@ fn do_dma(device: &mut device::Device) {
fn fifo_push(device: &mut device::Device) {
let duration = get_dma_duration(device);
if (device.ai.regs[AI_STATUS_REG as usize] & AI_STATUS_BUSY) != 0 {
if (device.ai.regs[AI_STATUS_REG] & AI_STATUS_BUSY) != 0 {
device.ai.fifo[1].address =
device.ai.regs[AI_DRAM_ADDR_REG as usize] as u64 & device::rdram::RDRAM_MASK as u64;
device.ai.fifo[1].length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
device.ai.regs[AI_DRAM_ADDR_REG] as u64 & device::rdram::RDRAM_MASK as u64;
device.ai.fifo[1].length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
device.ai.fifo[1].duration = duration;
device.ai.regs[AI_STATUS_REG as usize] |= AI_STATUS_FULL;
device.ai.regs[AI_STATUS_REG] |= AI_STATUS_FULL;
} else {
device.ai.fifo[0].address =
device.ai.regs[AI_DRAM_ADDR_REG as usize] as u64 & device::rdram::RDRAM_MASK as u64;
device.ai.fifo[0].length = (device.ai.regs[AI_LEN_REG as usize] & !7) as u64;
device.ai.regs[AI_DRAM_ADDR_REG] as u64 & device::rdram::RDRAM_MASK as u64;
device.ai.fifo[0].length = (device.ai.regs[AI_LEN_REG] & !7) as u64;
device.ai.fifo[0].duration = duration;
device.ai.regs[AI_STATUS_REG as usize] |= AI_STATUS_BUSY;
device.ai.regs[AI_STATUS_REG] |= AI_STATUS_BUSY;
do_dma(device);
ui::audio::resume_game_audio(&mut device.ui);
}
}
fn fifo_pop(device: &mut device::Device) {
if device.ai.regs[AI_STATUS_REG as usize] & AI_STATUS_FULL != 0 {
if device.ai.regs[AI_STATUS_REG] & AI_STATUS_FULL != 0 {
device.ai.fifo[0].address = device.ai.fifo[1].address;
device.ai.fifo[0].length = device.ai.fifo[1].length;
device.ai.fifo[0].duration = device.ai.fifo[1].duration;
device.ai.regs[AI_STATUS_REG as usize] &= !AI_STATUS_FULL;
device.ai.regs[AI_STATUS_REG] &= !AI_STATUS_FULL;
do_dma(device);
} else {
device.ai.regs[AI_STATUS_REG as usize] &= !AI_STATUS_BUSY;
device.ai.regs[AI_STATUS_REG] &= !AI_STATUS_BUSY;
device.ai.delayed_carry = false;
ui::audio::pause_game_audio(&mut device.ui);
}
}
@@ -110,7 +112,7 @@ pub fn read_regs(
_access_size: device::memory::AccessSize,
) -> u32 {
let reg = (address & 0xFFFF) >> 2;
let return_value = match reg as u32 {
let return_value = match reg as usize {
AI_LEN_REG => {
let value = get_remaining_dma_length(device);
if value < device.ai.last_read {
@@ -134,7 +136,7 @@ pub fn read_regs(
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
AI_LEN_REG => {
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask);
if device.ai.regs[reg as usize] != 0 {
@@ -145,8 +147,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
AI_DACRATE_REG => {
if device.ai.regs[reg as usize] != value & mask {
device.ai.freq = device.vi.clock / (1 + (value & mask)) as u64;
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq)
ui::audio::update_freq(device);
}
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
}
+6 -9
View File
@@ -21,7 +21,7 @@ const JDT_EEPROM_16K: u16 = 0xc000; /* 16k EEPROM */
const EEPROM_BLOCK_SIZE: usize = 8;
pub const EEPROM_MAX_SIZE: usize = 0x800;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct AfRtc {
pub control: u16,
}
@@ -31,7 +31,7 @@ fn byte2bcd(mut n: u32) -> u8 {
(((n / 10) << 4) | (n % 10)) as u8
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cart {
#[serde(skip)]
pub rom: Vec<u8>,
@@ -130,8 +130,6 @@ pub fn format_eeprom(device: &mut device::Device) {
fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
format_eeprom(device);
device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE].copy_from_slice(
&device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE],
);
@@ -140,8 +138,6 @@ fn eeprom_read_block(device: &mut device::Device, block: usize, offset: usize) {
fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize, status: usize) {
let address = device.pif.ram[block] as usize * EEPROM_BLOCK_SIZE;
format_eeprom(device);
device.ui.storage.saves.eeprom.data[address..address + EEPROM_BLOCK_SIZE]
.copy_from_slice(&device.pif.ram[offset..offset + EEPROM_BLOCK_SIZE]);
@@ -151,9 +147,10 @@ fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize,
}
fn time2data(device: &mut device::Device, offset: usize) {
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();
let timestamp = device.cart.rtc_timestamp + device.vi.elapsed_time as i64;
let now = chrono::DateTime::from_timestamp(timestamp, 0)
.unwrap()
.with_timezone(&chrono::Local);
device.pif.ram[offset] = byte2bcd(now.second());
device.pif.ram[offset + 1] = byte2bcd(now.minute());
+23 -46
View File
@@ -1,27 +1,18 @@
use crate::device;
use crate::netplay;
use crate::ui;
use sha2::{Digest, Sha256};
pub const CART_MASK: usize = 0xFFFFFFF;
fn read_cart_word(device: &device::Device, address: usize) -> u32 {
let mut data: [u8; 4] = [0; 4];
for i in 0..4 {
if let Some(value) = device
.ui
.storage
.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)
let romsave = &device.ui.storage.saves.romsave.data;
let rom = &device.cart.rom;
u32::from_be_bytes(std::array::from_fn(|i| {
romsave
.get(&((address + i) as u32))
.copied()
.unwrap_or_else(|| *rom.get(address + i).unwrap_or(&0))
}))
}
pub fn read_mem_fast(
@@ -41,8 +32,8 @@ pub fn read_mem(
let cycles = device::pi::calculate_cycles(device, 1, 4);
device::cop0::add_cycles(device, cycles);
// well known cart ROM oddity, if a read is perfomed while PI_STATUS_IO_BUSY is set, the latched value is returned rather than the data at the specified address
if device.pi.regs[device::pi::PI_STATUS_REG as usize] & device::pi::PI_STATUS_IO_BUSY != 0 {
// well known cart ROM oddity, if a read is performed while PI_STATUS_IO_BUSY is set, the latched value is returned rather than the data at the specified address
if device.pi.regs[device::pi::PI_STATUS_REG] & device::pi::PI_STATUS_IO_BUSY != 0 {
device.cart.latch
} else {
let masked_address = address as usize & CART_MASK;
@@ -69,7 +60,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
device.cart.latch = value & mask;
device.pi.regs[device::pi::PI_STATUS_REG as usize] |= device::pi::PI_STATUS_IO_BUSY;
device.pi.regs[device::pi::PI_STATUS_REG] |= device::pi::PI_STATUS_IO_BUSY;
let cycles = device::pi::calculate_cycles(device, 1, 4);
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
@@ -113,19 +104,15 @@ pub fn dma_write(
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length {
if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *value;
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
*value
} else {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *device.cart.rom.get(j as usize).unwrap_or(&0);
}
*device.cart.rom.get(j as usize).unwrap_or(&0)
};
i += 1;
j += 1;
}
@@ -134,20 +121,10 @@ pub fn dma_write(
}
pub fn init(device: &mut device::Device, rom_file: &[u8]) {
let now: chrono::DateTime<chrono::Local> = chrono::Local::now();
device.cart.rtc_timestamp = now.naive_local().and_utc().timestamp();
if let Some(netplay) = &mut device.netplay {
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.to_vec();
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
device.cart.pal = is_system_pal(&device.cart.rom);
set_cic(device);
device.ui.game_hash = calculate_hash(&device.cart.rom);
@@ -158,10 +135,10 @@ pub fn init(device: &mut device::Device, rom_file: &[u8]) {
}
}
fn is_system_pal(country: u8) -> bool {
let pal_codes: [u8; 8] = [b'D', b'F', b'I', b'P', b'S', b'U', b'X', b'Y'];
pub fn is_system_pal(rom_contents: &[u8]) -> bool {
let pal_codes: [u8; 8] = *b"DFIPSUXY";
for i in pal_codes {
if country == i {
if rom_contents[0x3E] == i {
return true;
}
}
+48 -68
View File
@@ -3,29 +3,29 @@ use crate::ui;
pub const SDCARD_SIZE: usize = 0x4000000;
const SC64_SCR_REG: u32 = 0;
const SC64_DATA0_REG: u32 = 1;
const SC64_DATA1_REG: u32 = 2;
const SC64_IDENTIFIER_REG: u32 = 3;
const SC64_KEY_REG: u32 = 4;
//const SC64_IRQ_REG: u32 = 5;
//const SC64_AUX_REG: u32 = 6;
pub const SC64_REGS_COUNT: u32 = 7;
const SC64_SCR_REG: usize = 0;
const SC64_DATA0_REG: usize = 1;
const SC64_DATA1_REG: usize = 2;
const SC64_IDENTIFIER_REG: usize = 3;
const SC64_KEY_REG: usize = 4;
//const SC64_IRQ_REG: usize = 5;
//const SC64_AUX_REG: usize = 6;
pub const SC64_REGS_COUNT: usize = 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;
pub const SC64_CFG_COUNT: usize = 15;
const SC64_BUFFER_MASK: usize = 0x1FFF;
const SC64_EEPROM_MASK: usize = 0xFFF;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Sc64 {
pub buffer: Vec<u8>,
pub regs: [u32; SC64_REGS_COUNT as usize],
pub regs: [u32; SC64_REGS_COUNT],
pub regs_locked: bool,
pub cfg: [u32; SC64_CFG_COUNT as usize],
pub cfg: [u32; SC64_CFG_COUNT],
pub sector: u32,
pub writeback_sector: Vec<u32>,
pub usb_buffer: Vec<u8>,
@@ -49,7 +49,7 @@ pub fn read_regs(
return 0;
}
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
SC64_SCR_REG | SC64_DATA0_REG | SC64_DATA1_REG => device.cart.sc64.regs[reg as usize],
SC64_IDENTIFIER_REG => 0x53437632,
_ => panic!("unknown read reg {reg} address {address:#x}"),
@@ -58,12 +58,12 @@ pub fn read_regs(
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
SC64_KEY_REG => {
device::memory::masked_write_32(&mut device.cart.sc64.regs[reg as usize], value, mask);
if device.cart.sc64.regs[SC64_KEY_REG as usize] == 0x4F434B5F {
if device.cart.sc64.regs[SC64_KEY_REG] == 0x4F434B5F {
device.cart.sc64.regs_locked = false;
} else if device.cart.sc64.regs[SC64_KEY_REG as usize] == 0xFFFFFFFF {
} else if device.cart.sc64.regs[SC64_KEY_REG] == 0xFFFFFFFF {
device.cart.sc64.regs_locked = true;
}
}
@@ -81,22 +81,22 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
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;
device.cart.sc64.regs[SC64_DATA0_REG] = (2 << 16) | 20;
device.cart.sc64.regs[SC64_DATA1_REG] = 2;
}
'c' => {
// get config
device.cart.sc64.regs[SC64_DATA1_REG as usize] = device.cart.sc64.cfg
[device.cart.sc64.regs[SC64_DATA0_REG as usize] as usize]
device.cart.sc64.regs[SC64_DATA1_REG] =
device.cart.sc64.cfg[device.cart.sc64.regs[SC64_DATA0_REG] as usize]
}
'C' => {
// set config
if device.cart.sc64.regs[SC64_DATA0_REG as usize] == SC64_SAVE_TYPE {
if device.cart.sc64.regs[SC64_DATA0_REG] == 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.storage.saves.write_to_disk = false;
device.ui.storage.save_type =
match device.cart.sc64.regs[SC64_DATA1_REG as usize] {
match device.cart.sc64.regs[SC64_DATA1_REG] {
0 => {
vec![]
}
@@ -115,20 +115,20 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
_ => {
panic!(
"unknown sc64 save type: {}",
device.cart.sc64.regs[SC64_DATA1_REG as usize]
device.cart.sc64.regs[SC64_DATA1_REG]
)
}
}
}
std::mem::swap(
&mut device.cart.sc64.cfg
[device.cart.sc64.regs[SC64_DATA0_REG as usize] as usize],
&mut device.cart.sc64.regs[SC64_DATA1_REG as usize],
[device.cart.sc64.regs[SC64_DATA0_REG] as usize],
&mut device.cart.sc64.regs[SC64_DATA1_REG],
);
}
'i' => {
// sd card operation
match device.cart.sc64.regs[SC64_DATA1_REG as usize] {
match device.cart.sc64.regs[SC64_DATA1_REG] {
0 => { //Init SD card
}
1 => { //Deinit SD card
@@ -136,23 +136,21 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
_ => {
panic!(
"unknown sc64 sd card operation: {}",
device.cart.sc64.regs[SC64_DATA1_REG as usize]
device.cart.sc64.regs[SC64_DATA1_REG]
)
}
}
}
'I' => {
// set sd sector
device.cart.sc64.sector = device.cart.sc64.regs[SC64_DATA0_REG as usize];
device.cart.sc64.sector = device.cart.sc64.regs[SC64_DATA0_REG];
}
's' => {
format_sdcard(device);
// read sd card
let address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
let offset = (device.cart.sc64.sector * 512) as usize;
let length =
(device.cart.sc64.regs[SC64_DATA1_REG as usize] * 512) as usize;
let length = (device.cart.sc64.regs[SC64_DATA1_REG] * 512) as usize;
let mut i = 0;
while i < length {
@@ -182,11 +180,9 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
'S' => {
format_sdcard(device);
// write sd card
let address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
let offset = (device.cart.sc64.sector * 512) as usize;
let length =
(device.cart.sc64.regs[SC64_DATA1_REG as usize] * 512) as usize;
let length = (device.cart.sc64.regs[SC64_DATA1_REG] * 512) as usize;
let mut i = 0;
while i < length {
@@ -211,42 +207,32 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sdcard);
}
'U' => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
device.cart.sc64.regs[SC64_DATA0_REG] = 0;
}
'u' => {
// used to notify the game that there is data to read
if let Some(cart_rx) = device.ui.usb.cart_rx.as_mut() {
match cart_rx.try_recv() {
Ok(data) => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] =
data.data_type & 0xFF; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG as usize] =
device.cart.sc64.regs[SC64_DATA0_REG] = data.data_type & 0xFF; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG] =
data.data_size & 0xFFFFFF; // length
device.cart.sc64.usb_buffer = data.data; // store the data to be read
}
Err(err) => {
match err {
tokio::sync::broadcast::error::TryRecvError::Lagged(_) => {
panic!("cart_rx lagged: {err}");
}
_ => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
}
}
Err(_err) => {
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
}
}
} else {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG as usize] = 0; // length
device.cart.sc64.regs[SC64_DATA0_REG] = 0; // read_status/type
device.cart.sc64.regs[SC64_DATA1_REG] = 0; // length
}
}
'M' => {
// Send data from from flashcart to USB
let address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
let length =
device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize & 0xFFFFFF;
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
let length = device.cart.sc64.regs[SC64_DATA1_REG] as usize & 0xFFFFFF;
if let Some(usb_tx) = device.ui.usb.usb_tx.as_mut() {
let mut usb_buffer = vec![0; length];
@@ -297,18 +283,16 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
usb_tx,
ui::usb::UsbData {
data: usb_buffer,
data_type: device.cart.sc64.regs[SC64_DATA1_REG as usize] >> 24,
data_size: device.cart.sc64.regs[SC64_DATA1_REG as usize]
& 0xFFFFFF,
data_type: device.cart.sc64.regs[SC64_DATA1_REG] >> 24,
data_size: device.cart.sc64.regs[SC64_DATA1_REG] & 0xFFFFFF,
},
);
}
}
'm' => {
// Receive data from USB to flashcart
let address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64 & 0x1FFFFFFF;
let length = device.cart.sc64.regs[SC64_DATA1_REG as usize] as usize;
let address = device.cart.sc64.regs[SC64_DATA0_REG] as u64 & 0x1FFFFFFF;
let length = device.cart.sc64.regs[SC64_DATA1_REG] as usize;
let mut i = 0;
@@ -338,11 +322,11 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
'w' => {
// SD card writeback pending
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
device.cart.sc64.regs[SC64_DATA0_REG] = 0;
}
'W' => {
let writeback_sectors_address =
device.cart.sc64.regs[SC64_DATA0_REG as usize] as u64;
device.cart.sc64.regs[SC64_DATA0_REG] as u64;
for i in 0..256 {
device.cart.sc64.writeback_sector[i] = device::memory::data_read(
device,
@@ -376,7 +360,6 @@ pub fn read_mem(
_access_size: device::memory::AccessSize,
) -> u32 {
if address & 0x2000 != 0 {
device::cart::format_eeprom(device);
let masked_address = address as usize & SC64_EEPROM_MASK;
u32::from_be_bytes(
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
@@ -395,7 +378,6 @@ pub fn read_mem(
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
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.storage.saves.eeprom.data[masked_address..masked_address + 4]
@@ -427,7 +409,6 @@ pub fn dma_read(
) -> u64 {
dram_addr &= device::rdram::RDRAM_MASK as u32;
let buffer = if cart_addr & 0x2000 != 0 {
device::cart::format_eeprom(device);
cart_addr &= SC64_EEPROM_MASK as u32;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
&mut device.ui.storage.saves.eeprom.data
@@ -459,7 +440,6 @@ pub fn dma_write(
) -> u64 {
dram_addr &= device::rdram::RDRAM_MASK as u32;
let buffer = if cart_addr & 0x2000 != 0 {
device::cart::format_eeprom(device);
cart_addr &= SC64_EEPROM_MASK as u32;
&device.ui.storage.saves.eeprom.data
} else {
+5 -17
View File
@@ -8,7 +8,7 @@ pub const MX29L1100_ID: u32 = 0x00c2001e;
const MX29L0000_ID: u32 = 0x00c20000;
const MX29L0001_ID: u32 = 0x00c20001;
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum FlashramMode {
ReadArray,
ReadSiliconId,
@@ -18,7 +18,7 @@ pub enum FlashramMode {
PageProgram,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Flashram {
pub status: u32,
pub mode: FlashramMode,
@@ -28,13 +28,13 @@ pub struct Flashram {
pub silicon_id: [u32; 2],
}
fn format_sram(device: &mut device::Device) {
pub fn format_sram(device: &mut device::Device) {
if device.ui.storage.saves.sram.data.len() < SRAM_SIZE {
device.ui.storage.saves.sram.data.resize(SRAM_SIZE, 0xFF)
}
}
fn format_flash(device: &mut device::Device) {
pub fn format_flash(device: &mut device::Device) {
if device.ui.storage.saves.flash.data.len() < FLASHRAM_SIZE {
device
.ui
@@ -49,8 +49,6 @@ fn format_flash(device: &mut device::Device) {
fn read_mem_sram(device: &mut device::Device, address: u64) -> u32 {
let masked_address = address as usize & SRAM_MASK;
format_sram(device);
u32::from_be_bytes(
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
.try_into()
@@ -96,8 +94,6 @@ pub fn read_mem(
fn write_mem_sram(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let masked_address = address as usize & SRAM_MASK;
format_sram(device);
let mut data = u32::from_be_bytes(
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
.try_into()
@@ -116,7 +112,6 @@ fn write_mem_flash(device: &mut device::Device, address: u64, value: u32, mask:
device.cart.flashram.status = (value & mask) & 0xff;
} else if (address & 0x1ffff) == 0x10000 {
/* set command */
format_flash(device);
flashram_command(device, value & mask);
} else {
/* other accesses are not implemented */
@@ -136,7 +131,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
write_mem_flash(device, address, value, mask)
}
device.pi.regs[device::pi::PI_STATUS_REG as usize] |= device::pi::PI_STATUS_IO_BUSY;
device.pi.regs[device::pi::PI_STATUS_REG] |= device::pi::PI_STATUS_IO_BUSY;
let cycles = device::pi::calculate_cycles(device, 2, 4);
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
@@ -148,8 +143,6 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
let mut i = dram_addr;
let mut j = cart_addr;
format_sram(device);
while i < dram_addr + length {
*device
.ui
@@ -171,8 +164,6 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
}
fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, length: u32) {
format_flash(device);
if (cart_addr & 0x1ffff) == 0x00000
&& length == 128
&& device.cart.flashram.mode == FlashramMode::PageProgram
@@ -217,8 +208,6 @@ fn dma_write_sram(
let mut i = dram_addr;
let mut j = cart_addr;
format_sram(device);
while i < dram_addr + length {
*device
.rdram
@@ -266,7 +255,6 @@ fn dma_write_flash(
} else if (cart_addr & 0x1ffff) < 0x10000
&& device.cart.flashram.mode == FlashramMode::ReadArray
{
format_flash(device);
/* adjust flashram address before starting DMA. */
if device.cart.flashram.silicon_id[1] == MX29L1100_ID
|| device.cart.flashram.silicon_id[1] == MX29L0000_ID
+18 -13
View File
@@ -1,4 +1,4 @@
use crate::{device, netplay, savestates, ui};
use crate::{device, savestates, ui};
pub mod gbcart;
pub mod mempak;
@@ -57,13 +57,9 @@ pub fn process(device: &mut device::Device, channel: usize) {
JCMD_CONTROLLER_READ => {
let offset = device.pif.channels[channel].rx_buf.unwrap();
let input = if let Some(netplay) = &mut device.netplay {
if netplay.player_number as usize == channel {
let local_input = ui::input::get(&device.ui, 0);
netplay::send_input(netplay, local_input);
}
netplay::get_input(device, channel)
netplay.inputs[channel].0
} else {
ui::input::get(&device.ui, channel)
ui::input::get(&mut device.ui, channel)
};
device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
@@ -76,7 +72,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
device::events::create_event(
device,
device::events::EVENT_TYPE_PAK,
device.cpu.clock_rate, // 1 second
device.cpu.clock_rate / 2, // 500ms
)
}
}
@@ -95,7 +91,7 @@ pub fn process(device: &mut device::Device, channel: usize) {
device.pif.channels[channel].rx_buf.unwrap(),
channel,
),
_ => println!("unknown controller command {cmd}"),
_ => eprintln!("unknown controller command {cmd}"),
}
}
@@ -162,7 +158,7 @@ pub fn pak_switch_event(device: &mut device::Device) {
if channel.change_pak != PakType::None {
//stop rumble if it is on
if let Some(netplay) = &device.netplay {
if netplay.player_number as usize == i {
if netplay.player_number == i {
device::ui::input::set_rumble(&device.ui, 0, 0);
}
} else {
@@ -190,21 +186,30 @@ pub fn pak_switch_event(device: &mut device::Device) {
write: device::controller::mempak::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message("MemPak selected", false);
ui::video::onscreen_message(
&format!("P{} MemPak selected", i + 1),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else if new_pak_type == PakType::RumblePak {
channel.pak_handler = Some(device::controller::PakHandler {
read: device::controller::rumble::read,
write: device::controller::rumble::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message("RumblePak selected", false);
ui::video::onscreen_message(
&format!("P{} RumblePak selected", i + 1),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else if new_pak_type == PakType::TransferPak {
channel.pak_handler = Some(device::controller::PakHandler {
read: device::controller::transferpak::read,
write: device::controller::transferpak::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message("TransferPak selected", false);
ui::video::onscreen_message(
&format!("P{} TransferPak selected", i + 1),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
}
channel.change_pak = PakType::None;
}
+156 -50
View File
@@ -1,6 +1,14 @@
use crate::device;
#[derive(Default, serde::Serialize, serde::Deserialize)]
use std::io::Write;
const MBC3_RTC_SECONDS: usize = 0;
const MBC3_RTC_MINUTES: usize = 1;
const MBC3_RTC_HOURS: usize = 2;
const MBC3_RTC_DAYS_L: usize = 3;
const MBC3_RTC_DAYS_H: usize = 4;
const MBC3_RTC_REGS_COUNT: usize = 5;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct GbCart {
pub enabled: bool,
#[serde(skip)]
@@ -11,14 +19,12 @@ pub struct GbCart {
pub mbc1_mode: 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,
pub rtc_regs: [u8; MBC3_RTC_REGS_COUNT],
pub rtc_regs_latch: [u8; MBC3_RTC_REGS_COUNT],
pub last_elapsed_time: i64,
}
#[derive(Default, Copy, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum CartType {
#[default]
None,
@@ -28,6 +34,135 @@ pub enum CartType {
MBC5RamBatt,
}
pub fn init(gb_cart: &mut device::controller::gbcart::GbCart, rom: &[u8], ram: &[u8]) {
gb_cart.rom = rom.to_vec();
let offset = match ram.len() % 8192 {
44 => {
gb_cart.ram = ram[..ram.len() - 44].to_vec();
ram.len() - 44
}
48 => {
gb_cart.ram = ram[..ram.len() - 48].to_vec();
ram.len() - 48
}
_ => {
gb_cart.ram = ram.to_vec();
return;
}
};
gb_cart.rtc_regs[MBC3_RTC_SECONDS] =
u32::from_le_bytes(ram[offset..offset + 4].try_into().unwrap()) as u8;
gb_cart.rtc_regs[MBC3_RTC_MINUTES] =
u32::from_le_bytes(ram[offset + 4..offset + 8].try_into().unwrap()) as u8;
gb_cart.rtc_regs[MBC3_RTC_HOURS] =
u32::from_le_bytes(ram[offset + 8..offset + 12].try_into().unwrap()) as u8;
gb_cart.rtc_regs[MBC3_RTC_DAYS_L] =
u32::from_le_bytes(ram[offset + 12..offset + 16].try_into().unwrap()) as u8;
gb_cart.rtc_regs[MBC3_RTC_DAYS_H] =
u32::from_le_bytes(ram[offset + 16..offset + 20].try_into().unwrap()) as u8;
gb_cart.rtc_regs_latch[MBC3_RTC_SECONDS] =
u32::from_le_bytes(ram[offset + 20..offset + 24].try_into().unwrap()) as u8;
gb_cart.rtc_regs_latch[MBC3_RTC_MINUTES] =
u32::from_le_bytes(ram[offset + 24..offset + 28].try_into().unwrap()) as u8;
gb_cart.rtc_regs_latch[MBC3_RTC_HOURS] =
u32::from_le_bytes(ram[offset + 28..offset + 32].try_into().unwrap()) as u8;
gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_L] =
u32::from_le_bytes(ram[offset + 32..offset + 36].try_into().unwrap()) as u8;
gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_H] =
u32::from_le_bytes(ram[offset + 36..offset + 40].try_into().unwrap()) as u8;
}
pub fn save(
gb_cart: &mut device::controller::gbcart::GbCart,
elapsed_time: i64,
ram_path: &std::string::String,
) {
if cfg!(target_os = "android") {
return;
}
update_rtc_regs(gb_cart, elapsed_time);
if let Ok(file) = std::fs::File::create(ram_path) {
let mut f = std::io::BufWriter::new(file);
f.write_all(&gb_cart.ram).unwrap();
if gb_cart.cart_type == CartType::MBC3RamBattRtc {
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_SECONDS] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_MINUTES] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_HOURS] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_DAYS_L] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs[MBC3_RTC_DAYS_H] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_SECONDS] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_MINUTES] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_HOURS] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_L] as u32).to_le_bytes())
.unwrap();
f.write_all(&(gb_cart.rtc_regs_latch[MBC3_RTC_DAYS_H] as u32).to_le_bytes())
.unwrap();
let max = ((i32::MAX as i64) << 32) | (i32::MAX as i64);
f.write_all(&max.to_le_bytes()).unwrap();
f.flush().unwrap();
}
} else {
eprintln!("Error saving TransferPak RAM to {ram_path}");
}
}
fn update_rtc_regs(cart: &mut device::controller::gbcart::GbCart, elapsed_time: i64) {
let mut diff = elapsed_time - cart.last_elapsed_time;
cart.last_elapsed_time = elapsed_time;
if diff > 0 {
cart.rtc_regs[MBC3_RTC_SECONDS] += (diff % 60) as u8;
if cart.rtc_regs[MBC3_RTC_SECONDS] >= 60 {
cart.rtc_regs[MBC3_RTC_SECONDS] -= 60;
cart.rtc_regs[MBC3_RTC_MINUTES] += 1;
}
diff /= 60;
cart.rtc_regs[MBC3_RTC_MINUTES] += (diff % 60) as u8;
if cart.rtc_regs[MBC3_RTC_MINUTES] >= 60 {
cart.rtc_regs[MBC3_RTC_MINUTES] -= 60;
cart.rtc_regs[MBC3_RTC_HOURS] += 1;
}
diff /= 60;
cart.rtc_regs[MBC3_RTC_HOURS] += (diff % 24) as u8;
if cart.rtc_regs[MBC3_RTC_HOURS] >= 24 {
cart.rtc_regs[MBC3_RTC_HOURS] -= 24;
cart.rtc_regs[MBC3_RTC_DAYS_L] += 1;
}
diff /= 24;
/* update days counter */
let days = ((((cart.rtc_regs[MBC3_RTC_DAYS_H] & 0x01) as i64) << 8)
| cart.rtc_regs[MBC3_RTC_DAYS_L] as i64)
+ diff;
cart.rtc_regs[MBC3_RTC_DAYS_L] = (days & 0xff) as u8;
cart.rtc_regs[MBC3_RTC_DAYS_H] &= !0x01;
cart.rtc_regs[MBC3_RTC_DAYS_H] |= ((days >> 8) & 1) as u8;
/* set carry bit if days overflow */
if days > 511 {
cart.rtc_regs[MBC3_RTC_DAYS_H] |= 0x80;
}
}
}
fn write_mbc1(
pif_ram: &[u8],
cart: &mut device::controller::gbcart::GbCart,
@@ -105,6 +240,7 @@ fn write_mbc3(
address: u16,
data: usize,
size: usize,
elapsed_time: i64,
) {
let value = pif_ram[data + size - 1];
if address < 0x2000 {
@@ -118,10 +254,10 @@ fn write_mbc3(
} else if address < 0x6000 {
cart.ram_bank = (value & 0xf) as u16;
} else if address < 0x8000 {
if !cart.set_latch && value != 0 {
// not implemented
if cart.cart_type == CartType::MBC3RamBattRtc && (value & 0x1) != 0 {
update_rtc_regs(cart, elapsed_time);
cart.rtc_regs_latch = cart.rtc_regs;
}
cart.set_latch = value != 0;
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
return;
@@ -130,29 +266,8 @@ fn write_mbc3(
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 if cart.cart_type == CartType::MBC3RamBattRtc {
cart.rtc_regs[cart.ram_bank as usize - 0x8] = value;
}
} else {
panic!("Unsupported write address {address:x}");
@@ -161,7 +276,7 @@ fn write_mbc3(
fn read_mbc3(
pif_ram: &mut [u8],
cart: &device::controller::gbcart::GbCart,
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
@@ -188,19 +303,9 @@ fn read_mbc3(
.get(banked_address as usize..banked_address as usize + size)
.unwrap_or(&vec![0; 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);
}
};
} else if cart.cart_type == CartType::MBC3RamBattRtc {
for i in 0..size {
pif_ram[data + i] = latch;
pif_ram[data + i] = cart.rtc_regs_latch[cart.ram_bank as usize - 0x8];
}
}
} else {
@@ -227,7 +332,7 @@ fn write_mbc5(
} else if address < 0x6000 {
cart.ram_bank = (value & 0xf) as u16;
} else if address < 0xa000 {
println!("Unknown MBC5 write address {address:x}");
eprintln!("Unknown MBC5 write address {address:x}");
} else if (0xa000..0xc000).contains(&address) {
if !cart.ram_enabled {
return;
@@ -274,7 +379,7 @@ fn read_mbc5(
pub fn read(
pif_ram: &mut [u8],
cart: &device::controller::gbcart::GbCart,
cart: &mut device::controller::gbcart::GbCart,
address: u16,
data: usize,
size: usize,
@@ -300,14 +405,15 @@ pub fn write(
address: u16,
data: usize,
size: usize,
elapsed_time: i64,
) {
if !cart.enabled {
return;
}
match cart.cart_type {
CartType::MBC1RamBatt => write_mbc1(pif_ram, cart, address, data, size),
CartType::MBC3RamBatt => write_mbc3(pif_ram, cart, address, data, size),
CartType::MBC3RamBattRtc => write_mbc3(pif_ram, cart, address, data, size),
CartType::MBC3RamBatt => write_mbc3(pif_ram, cart, address, data, size, elapsed_time),
CartType::MBC3RamBattRtc => write_mbc3(pif_ram, cart, address, data, size, elapsed_time),
CartType::MBC5RamBatt => write_mbc5(pif_ram, cart, address, data, size),
_ => panic!("Unsupported cart type"),
}
+6 -5
View File
@@ -4,7 +4,7 @@ use crate::ui;
pub const MEMPAK_SIZE: usize = 0x8000;
const MPK_PAGE_SIZE: usize = 256;
fn format_mempak(device: &mut device::Device) {
pub fn format_mempak(device: &mut device::Device) {
if device.ui.storage.saves.mempak.data.len() < MEMPAK_SIZE * 4 {
device
.ui
@@ -43,6 +43,11 @@ fn format_mempak(device: &mut device::Device) {
];
for i in 0..4 {
let offset = i * MEMPAK_SIZE;
if device.ui.storage.saves.mempak.data[offset..offset + MEMPAK_SIZE] != [0; MEMPAK_SIZE]
{
// don't overwrite existing data
continue;
}
/* Fill Page 0 with pre-initialized content */
device.ui.storage.saves.mempak.data[offset..offset + MPK_PAGE_SIZE]
@@ -77,8 +82,6 @@ fn format_mempak(device: &mut device::Device) {
pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
if (address as usize) < MEMPAK_SIZE {
format_mempak(device);
let offset = (channel * MEMPAK_SIZE) + address as usize;
device.pif.ram[data..data + size]
.copy_from_slice(&device.ui.storage.saves.mempak.data[offset..offset + size])
@@ -91,8 +94,6 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
if (address as usize) < MEMPAK_SIZE {
format_mempak(device);
let offset = (channel * MEMPAK_SIZE) + address as usize;
device.ui.storage.saves.mempak.data[offset..offset + size]
.copy_from_slice(&device.pif.ram[data..data + size]);
+1 -1
View File
@@ -17,7 +17,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
let rumble = device.pif.ram[data + size - 1];
if let Some(netplay) = &device.netplay {
if netplay.player_number as usize == channel {
if netplay.player_number == channel {
device::ui::input::set_rumble(&device.ui, 0, rumble);
}
} else {
+6 -5
View File
@@ -1,6 +1,6 @@
use crate::device;
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct TransferPak {
pub enabled: bool,
pub reset_state: u8,
@@ -54,14 +54,14 @@ pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usi
0xC..=0xF => {
device::controller::gbcart::read(
&mut device.pif.ram,
&pak.cart,
&mut pak.cart,
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
data,
size,
);
}
_ => {
println!("unknown transfer pak read {:x}", address >> 12);
eprintln!("unknown transfer pak read {:x}", address >> 12);
}
}
}
@@ -84,7 +84,7 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
}
pak.enabled = true;
}
_ => println!("unknown transfer pak value"),
_ => eprintln!("unknown transfer pak value"),
}
return;
}
@@ -124,10 +124,11 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
0x4000 * pak.bank + (address & 0x7fff) - 0x4000,
data,
size,
device.vi.elapsed_time as i64,
);
}
_ => {
println!("unknown transfer pak write {:x}", address >> 12);
eprintln!("unknown transfer pak write {:x}", address >> 12);
}
}
}
+7 -8
View File
@@ -1,7 +1,6 @@
use crate::device;
#[cfg(feature = "gui")]
#[cfg(all(feature = "gui", not(target_os = "android")))]
use crate::ui;
use std::collections::HashMap;
const JCMD_VRU_READ: u8 = 0x09;
const JCMD_VRU_WRITE: u8 = 0x0A;
@@ -18,7 +17,7 @@ const VOICE_STATUS_BUSY: u8 = 0x05;
const JDT_VRU: u16 = 0x0100; /* VRU */
const CONT_FLAVOR: u16 = JDT_VRU;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Vru {
pub status: u8,
pub voice_state: u8,
@@ -28,7 +27,7 @@ pub struct Vru {
pub word_buffer: [u16; 40],
pub words: Vec<String>,
pub talking: bool,
pub word_mappings: HashMap<String, String>,
pub word_mappings: rustc_hash::FxHashMap<String, String>,
}
pub fn init(device: &mut device::Device) {
@@ -181,12 +180,12 @@ pub fn process(device: &mut device::Device, channel: usize) {
device.pif.ram[device.pif.channels[channel].rx_buf.unwrap()] = 0;
}
JCMD_VRU_READ => {
#[cfg(feature = "gui")]
#[cfg(all(feature = "gui", not(target_os = "android")))]
let index = ui::vru::prompt_for_match(&device.vru.words, device.vi.frame_time);
#[cfg(not(feature = "gui"))]
#[cfg(any(not(feature = "gui"), target_os = "android"))]
let index = 0x7FFF;
let num_results = if index == 0x7FFF { 0 } else { 1 };
let data: HashMap<usize, u16> = HashMap::from([
let data: rustc_hash::FxHashMap<usize, u16> = rustc_hash::FxHashMap::from_iter([
(0, 0x8000),
(2, 0x0F00),
(4, 0), // error flags
@@ -247,7 +246,7 @@ pub fn vru_talking_event(device: &mut device::Device) {
}
fn create_word_mappings(device: &mut device::Device) {
device.vru.word_mappings = HashMap::from([
device.vru.word_mappings = rustc_hash::FxHashMap::from_iter([
(
String::from("03A50024000303CF00A80003036000EA"),
String::from("pikachu"),
+68 -73
View File
@@ -1,38 +1,38 @@
use crate::{device, savestates};
pub const COP0_INDEX_REG: u32 = 0;
const COP0_RANDOM_REG: u32 = 1;
pub const COP0_ENTRYLO0_REG: u32 = 2;
pub const COP0_ENTRYLO1_REG: u32 = 3;
pub const COP0_CONTEXT_REG: u32 = 4;
pub const COP0_PAGEMASK_REG: u32 = 5;
const COP0_WIRED_REG: u32 = 6;
//const COP0_UNUSED_7: u32 = 7;
pub const COP0_BADVADDR_REG: u32 = 8;
pub const COP0_COUNT_REG: u32 = 9;
pub const COP0_ENTRYHI_REG: u32 = 10;
const COP0_COMPARE_REG: u32 = 11;
pub const COP0_STATUS_REG: u32 = 12;
pub const COP0_CAUSE_REG: u32 = 13;
pub const COP0_EPC_REG: u32 = 14;
const COP0_PREVID_REG: u32 = 15;
const COP0_CONFIG_REG: u32 = 16;
pub const COP0_LLADDR_REG: u32 = 17;
//const COP0_WATCHLO_REG: u32 = 18;
//const COP0_WATCHHI_REG: u32 = 19;
pub const COP0_XCONTEXT_REG: u32 = 20;
//const COP0_UNUSED_21: u32 = 21;
//const COP0_UNUSED_22: u32 = 22;
//const COP0_UNUSED_23: u32 = 23;
//const COP0_UNUSED_24: u32 = 24;
//const COP0_UNUSED_25: u32 = 25;
//const COP0_PARITYERR_REG: u32 = 26;
//const COP0_CACHEERR_REG: u32 = 27;
pub const COP0_TAGLO_REG: u32 = 28;
//const COP0_TAGHI_REG: u32 = 29;
pub const COP0_ERROREPC_REG: u32 = 30;
//const COP0_UNUSED_31: u32 = 31;
pub const COP0_REGS_COUNT: u32 = 32;
pub const COP0_INDEX_REG: usize = 0;
const COP0_RANDOM_REG: usize = 1;
pub const COP0_ENTRYLO0_REG: usize = 2;
pub const COP0_ENTRYLO1_REG: usize = 3;
pub const COP0_CONTEXT_REG: usize = 4;
pub const COP0_PAGEMASK_REG: usize = 5;
const COP0_WIRED_REG: usize = 6;
//const COP0_UNUSED_7: usize = 7;
pub const COP0_BADVADDR_REG: usize = 8;
pub const COP0_COUNT_REG: usize = 9;
pub const COP0_ENTRYHI_REG: usize = 10;
const COP0_COMPARE_REG: usize = 11;
pub const COP0_STATUS_REG: usize = 12;
pub const COP0_CAUSE_REG: usize = 13;
pub const COP0_EPC_REG: usize = 14;
const COP0_PREVID_REG: usize = 15;
const COP0_CONFIG_REG: usize = 16;
pub const COP0_LLADDR_REG: usize = 17;
//const COP0_WATCHLO_REG: usize = 18;
//const COP0_WATCHHI_REG: usize = 19;
pub const COP0_XCONTEXT_REG: usize = 20;
//const COP0_UNUSED_21: usize = 21;
//const COP0_UNUSED_22: usize = 22;
//const COP0_UNUSED_23: usize = 23;
//const COP0_UNUSED_24: usize = 24;
//const COP0_UNUSED_25: usize = 25;
//const COP0_PARITYERR_REG: usize = 26;
//const COP0_CACHEERR_REG: usize = 27;
pub const COP0_TAGLO_REG: usize = 28;
//const COP0_TAGHI_REG: usize = 29;
pub const COP0_ERROREPC_REG: usize = 30;
//const COP0_UNUSED_31: usize = 31;
pub const COP0_REGS_COUNT: usize = 32;
pub const COP0_STATUS_IE: u64 = 1 << 0;
pub const COP0_STATUS_EXL: u64 = 1 << 1;
@@ -91,17 +91,17 @@ const COP0_XCONTEXT_REG_MASK: u64 =
const COP0_PARITYERR_REG_MASK: u64 = 0b00000000000000000000000011111111;
const COP0_TAGLO_REG_MASK: u64 = 0b00001111111111111111111111000000;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cop0 {
pub reg_latch: u64,
pub regs: [u64; COP0_REGS_COUNT as usize],
pub reg_write_masks: [u64; COP0_REGS_COUNT as usize],
pub regs: [u64; COP0_REGS_COUNT],
pub reg_write_masks: [u64; COP0_REGS_COUNT],
#[serde(skip, default = "savestates::default_instructions")]
pub instrs: [fn(&mut device::Device, u32); 32],
#[serde(skip, default = "savestates::default_instructions")]
pub instrs2: [fn(&mut device::Device, u32); 32],
pub tlb_lut_r: Vec<device::tlb::TlbLut>,
pub tlb_lut_w: Vec<device::tlb::TlbLut>,
pub tlb_lut_r: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
pub tlb_lut_w: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
pub tlb_entries: [device::tlb::TlbEntry; 32],
pub is_event: bool,
}
@@ -136,11 +136,11 @@ fn dmtc0(device: &mut device::Device, opcode: u32) {
}
fn tlbr(device: &mut device::Device, _opcode: u32) {
device::tlb::read(device, device.cpu.cop0.regs[COP0_INDEX_REG as usize])
device::tlb::read(device, device.cpu.cop0.regs[COP0_INDEX_REG])
}
fn tlbwi(device: &mut device::Device, _opcode: u32) {
device::tlb::write(device, device.cpu.cop0.regs[COP0_INDEX_REG as usize])
device::tlb::write(device, device.cpu.cop0.regs[COP0_INDEX_REG])
}
fn tlbwr(device: &mut device::Device, _opcode: u32) {
@@ -153,12 +153,12 @@ fn tlbp(device: &mut device::Device, _opcode: u32) {
}
fn eret(device: &mut device::Device, _opcode: u32) {
if device.cpu.cop0.regs[COP0_STATUS_REG as usize] & COP0_STATUS_ERL != 0 {
device.cpu.pc = device.cpu.cop0.regs[COP0_ERROREPC_REG as usize];
device.cpu.cop0.regs[COP0_STATUS_REG as usize] &= !COP0_STATUS_ERL
if device.cpu.cop0.regs[COP0_STATUS_REG] & COP0_STATUS_ERL != 0 {
device.cpu.pc = device.cpu.cop0.regs[COP0_ERROREPC_REG];
device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_ERL
} else {
device.cpu.pc = device.cpu.cop0.regs[COP0_EPC_REG as usize];
device.cpu.cop0.regs[COP0_STATUS_REG as usize] &= !COP0_STATUS_EXL
device.cpu.pc = device.cpu.cop0.regs[COP0_EPC_REG];
device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_EXL
}
device.cpu.branch_state.state = device::cpu::State::Exception;
device.cpu.llbit = false;
@@ -184,7 +184,7 @@ pub fn reserved(device: &mut device::Device, _opcode: u32) {
pub fn emux_xdetect(_device: &mut device::Device, _opcode: u32) {}
fn get_control_registers(device: &device::Device, index: u32) -> u64 {
match index {
match index as usize {
COP0_COUNT_REG => device.cpu.cop0.regs[index as usize] >> 1,
COP0_RANDOM_REG => set_random_register(device),
7 | 21 | 22 | 23 | 24 | 25 | 31 => device.cpu.cop0.reg_latch,
@@ -194,22 +194,18 @@ fn get_control_registers(device: &device::Device, index: u32) -> u64 {
fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64) {
device.cpu.cop0.reg_latch = data;
match index {
match index as usize {
COP0_COUNT_REG => {
data &= 0xFFFFFFFF;
data <<= 1;
device::events::translate_events(
device,
device.cpu.cop0.regs[COP0_COUNT_REG as usize],
data,
);
device.cpu.cop0.regs[COP0_COUNT_REG as usize] = data;
device::events::translate_events(device, device.cpu.cop0.regs[COP0_COUNT_REG], data);
device.cpu.cop0.regs[COP0_COUNT_REG] = data;
return;
}
COP0_WIRED_REG => device.cpu.cop0.regs[COP0_RANDOM_REG as usize] = 31,
COP0_WIRED_REG => device.cpu.cop0.regs[COP0_RANDOM_REG] = 31,
COP0_COMPARE_REG => {
data &= 0xFFFFFFFF;
let current_count = (device.cpu.cop0.regs[COP0_COUNT_REG as usize] >> 1) & 0xFFFFFFFF;
let current_count = (device.cpu.cop0.regs[COP0_COUNT_REG] >> 1) & 0xFFFFFFFF;
let mut compare_event_diff = (data as u32).wrapping_sub(current_count as u32);
if compare_event_diff == 0 {
@@ -221,7 +217,7 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
device::events::EVENT_TYPE_COMPARE,
(compare_event_diff as u64) << 1,
);
device.cpu.cop0.regs[COP0_CAUSE_REG as usize] &= !COP0_CAUSE_IP7;
device.cpu.cop0.regs[COP0_CAUSE_REG] &= !COP0_CAUSE_IP7;
}
COP0_STATUS_REG
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR =>
@@ -239,9 +235,8 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
}
pub fn compare_event(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP7;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP7;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_EXCCODE_MASK;
device::events::create_event_at(
device,
@@ -252,17 +247,17 @@ pub fn compare_event(device: &mut device::Device) {
}
fn set_random_register(device: &device::Device) -> u64 {
if device.cpu.cop0.regs[COP0_WIRED_REG as usize] > 31 {
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG as usize]) & 0x3F
if device.cpu.cop0.regs[COP0_WIRED_REG] > 31 {
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG]) & 0x3F
} else {
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG as usize])
% (32 - device.cpu.cop0.regs[COP0_WIRED_REG as usize])
+ device.cpu.cop0.regs[COP0_WIRED_REG as usize]
(u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG])
% (32 - device.cpu.cop0.regs[COP0_WIRED_REG])
+ device.cpu.cop0.regs[COP0_WIRED_REG]
}
}
pub fn add_cycles(device: &mut device::Device, cycles: u64) {
device.cpu.cop0.regs[COP0_COUNT_REG as usize] += cycles // COUNT_REG is shifted right 1 bit when read by MFC0
device.cpu.cop0.regs[COP0_COUNT_REG] += cycles // COUNT_REG is shifted right 1 bit when read by MFC0
}
pub fn map_instructions(device: &mut device::Device) {
@@ -376,14 +371,14 @@ pub fn init(device: &mut device::Device) {
map_instructions(device);
// taken from VR4300 manual
device.cpu.cop0.regs[COP0_RANDOM_REG as usize] = 0b00000000000000000000000000011111;
device.cpu.cop0.regs[COP0_CONFIG_REG as usize] = 0b01110000000001101110010001100000;
device.cpu.cop0.regs[COP0_STATUS_REG as usize] = 0b00000000010000000000000000000100;
device.cpu.cop0.regs[COP0_PREVID_REG as usize] = 0b00000000000000000000101100100010;
device.cpu.cop0.regs[COP0_EPC_REG as usize] = 0b11111111111111111111111111111111;
device.cpu.cop0.regs[COP0_ERROREPC_REG as usize] = 0b11111111111111111111111111111111;
device.cpu.cop0.regs[COP0_BADVADDR_REG as usize] = 0xFFFFFFFF;
device.cpu.cop0.regs[COP0_CONTEXT_REG as usize] = 0x7FFFF0;
device.cpu.cop0.regs[COP0_RANDOM_REG] = 0b00000000000000000000000000011111;
device.cpu.cop0.regs[COP0_CONFIG_REG] = 0b01110000000001101110010001100000;
device.cpu.cop0.regs[COP0_STATUS_REG] = 0b00000000010000000000000000000100;
device.cpu.cop0.regs[COP0_PREVID_REG] = 0b00000000000000000000101100100010;
device.cpu.cop0.regs[COP0_EPC_REG] = 0b11111111111111111111111111111111;
device.cpu.cop0.regs[COP0_ERROREPC_REG] = 0b11111111111111111111111111111111;
device.cpu.cop0.regs[COP0_BADVADDR_REG] = 0xFFFFFFFF;
device.cpu.cop0.regs[COP0_CONTEXT_REG] = 0x7FFFF0;
device::events::create_event(device, device::events::EVENT_TYPE_COMPARE, u32::MAX as u64)
}
+23 -67
View File
@@ -24,7 +24,7 @@ const FCR31_CAUSE_MASK: u32 = 0b00000000000000111111000000000000;
const FCR31_ENABLE_MASK: u32 = 0b00000000000000000000111110000000;
const FCR31_WRITE_MASK: u32 = 0b00000001100000111111111111111111;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cop1 {
pub fcr0: u32,
pub fcr31: u32,
@@ -47,9 +47,7 @@ pub struct Cop1 {
}
pub fn lwc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -79,9 +77,7 @@ pub fn lwc1(device: &mut device::Device, opcode: u32) {
}
pub fn ldc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -118,9 +114,7 @@ pub fn ldc1(device: &mut device::Device, opcode: u32) {
}
pub fn swc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -145,9 +139,7 @@ pub fn swc1(device: &mut device::Device, opcode: u32) {
}
pub fn sdc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -181,9 +173,7 @@ pub fn sdc1(device: &mut device::Device, opcode: u32) {
}
fn mfc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -193,9 +183,7 @@ fn mfc1(device: &mut device::Device, opcode: u32) {
}
fn dmfc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -205,9 +193,7 @@ fn dmfc1(device: &mut device::Device, opcode: u32) {
}
fn cfc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -217,9 +203,7 @@ fn cfc1(device: &mut device::Device, opcode: u32) {
}
fn dcfc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -229,9 +213,7 @@ fn dcfc1(device: &mut device::Device, opcode: u32) {
}
fn mtc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -247,9 +229,7 @@ fn mtc1(device: &mut device::Device, opcode: u32) {
}
fn dmtc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -260,9 +240,7 @@ fn dmtc1(device: &mut device::Device, opcode: u32) {
}
fn ctc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -274,9 +252,7 @@ fn ctc1(device: &mut device::Device, opcode: u32) {
}
fn dctc1(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -286,9 +262,7 @@ fn dctc1(device: &mut device::Device, opcode: u32) {
}
fn execute_cop1_b(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -296,9 +270,7 @@ fn execute_cop1_b(device: &mut device::Device, opcode: u32) {
}
fn execute_cop1_s(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -306,9 +278,7 @@ fn execute_cop1_s(device: &mut device::Device, opcode: u32) {
}
fn execute_cop1_d(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -316,9 +286,7 @@ fn execute_cop1_d(device: &mut device::Device, opcode: u32) {
}
fn execute_cop1_l(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -326,9 +294,7 @@ fn execute_cop1_l(device: &mut device::Device, opcode: u32) {
}
fn execute_cop1_w(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -340,9 +306,7 @@ fn unusable(device: &mut device::Device, _opcode: u32) {
}
pub fn reserved(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU1
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU1 == 0 {
return unusable(device, opcode);
}
@@ -406,9 +370,7 @@ fn set_control_registers_fpu(device: &mut device::Device, index: u32, data: u32)
pub fn set_fpr_single(device: &mut device::Device, index: usize, value: f32, clear_high: bool) {
let bytes = value.to_ne_bytes();
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
device.cpu.cop1.fgr32[index] = bytes;
} else {
let bytes_lo = bytes;
@@ -422,9 +384,7 @@ pub fn set_fpr_single(device: &mut device::Device, index: usize, value: f32, cle
}
pub fn get_fpr_single(device: &device::Device, index: usize) -> f32 {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
f32::from_ne_bytes(device.cpu.cop1.fgr32[index])
} else {
f32::from_ne_bytes(device.cpu.cop1.fgr64[index][0..4].try_into().unwrap())
@@ -433,9 +393,7 @@ pub fn get_fpr_single(device: &device::Device, index: usize) -> f32 {
pub fn set_fpr_double(device: &mut device::Device, index: usize, value: f64) {
let bytes = value.to_ne_bytes();
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
device.cpu.cop1.fgr32[index & !1] = bytes[0..4].try_into().unwrap();
device.cpu.cop1.fgr32[(index & !1) + 1] = bytes[4..8].try_into().unwrap();
} else {
@@ -444,9 +402,7 @@ pub fn set_fpr_double(device: &mut device::Device, index: usize, value: f64) {
}
pub fn get_fpr_double(device: &device::Device, index: usize) -> f64 {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_FR
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_FR == 0 {
let bytes_lo = device.cpu.cop1.fgr32[index & !1];
let bytes_hi = device.cpu.cop1.fgr32[(index & !1) + 1];
f64::from_ne_bytes([bytes_lo, bytes_hi].concat().try_into().unwrap())
+8 -22
View File
@@ -1,6 +1,6 @@
use crate::{device, savestates};
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cop2 {
#[serde(skip, default = "savestates::default_instructions")]
pub instrs: [fn(&mut device::Device, u32); 32],
@@ -8,9 +8,7 @@ pub struct Cop2 {
}
fn mfc2(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
return unusable(device, opcode);
}
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] =
@@ -18,18 +16,14 @@ fn mfc2(device: &mut device::Device, opcode: u32) {
}
fn dmfc2(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
return unusable(device, opcode);
}
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] = device.cpu.cop2.reg_latch
}
fn cfc2(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
return unusable(device, opcode);
}
device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] =
@@ -37,27 +31,21 @@ fn cfc2(device: &mut device::Device, opcode: u32) {
}
fn mtc2(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
return unusable(device, opcode);
}
device.cpu.cop2.reg_latch = device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize]
}
fn dmtc2(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
return unusable(device, opcode);
}
device.cpu.cop2.reg_latch = device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize]
}
fn ctc2(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
return unusable(device, opcode);
}
@@ -65,9 +53,7 @@ fn ctc2(device: &mut device::Device, opcode: u32) {
}
pub fn reserved(device: &mut device::Device, opcode: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_CU2
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_CU2 == 0 {
return unusable(device, opcode);
}
+8 -6
View File
@@ -1,6 +1,6 @@
use crate::{device, savestates, ui};
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum State {
Step,
Take,
@@ -11,13 +11,13 @@ pub enum State {
Exception,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct BranchState {
pub state: State,
pub pc: u64,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cpu {
pub cop0: device::cop0::Cop0,
pub cop1: device::cop1::Cop1,
@@ -227,7 +227,10 @@ pub fn init(device: &mut device::Device) {
device.cpu.clock_rate = if !device.cpu.overclock {
93750000
} else {
ui::video::onscreen_message("Overclocking enabled, setting clock rate to 125 MHz", false);
ui::video::onscreen_message(
"Overclocking enabled, setting clock rate to 125 MHz",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
125000000
};
@@ -297,8 +300,7 @@ pub fn run(device: &mut device::Device) {
State::Exception => device.cpu.branch_state.state = State::Step,
}
device::cop0::add_cycles(device, 1);
if device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] > device.cpu.next_event_count
{
if device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] > device.cpu.next_event_count {
device::events::trigger_event(device)
}
}
+16 -18
View File
@@ -56,7 +56,7 @@ pub fn check_relative_idle_loop(device: &mut device::Device, opcode: u32) {
device::memory::AccessSize::Word,
) == 0
{
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] = device.cpu.next_event_count
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] = device.cpu.next_event_count
}
}
@@ -68,7 +68,7 @@ fn check_absolute_idle_loop(device: &mut device::Device, target: u64) {
device::memory::AccessSize::Word,
) == 0
{
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] = device.cpu.next_event_count
device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] = device.cpu.next_event_count
}
}
@@ -647,17 +647,17 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
valid = 1
}
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
valid << 7,
0x80,
);
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
0,
0x40,
);
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
(device.memory.icache[icache_line].tag >> 4) as u64,
0xFFFFF00,
)
@@ -665,10 +665,9 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
0x08 => {
//icache store tag
device.memory.icache[icache_line].valid =
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x80) >> 7 != 0;
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x80) >> 7 != 0;
device.memory.icache[icache_line].tag =
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0xFFFFF00) << 4)
as u32
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0xFFFFF00) << 4) as u32
}
0x10 => {
//icache hit invalidate
@@ -704,17 +703,17 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
dirty = 1
}
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
valid << 7,
0x80,
);
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
dirty << 6,
0x40,
);
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG],
(device.memory.dcache[dcache_line].tag >> 4) as u64,
0xFFFFF00,
)
@@ -722,12 +721,11 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
0x09 => {
//dcache index store tag
device.memory.dcache[dcache_line].valid =
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x80) >> 7 != 0;
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x80) >> 7 != 0;
device.memory.dcache[dcache_line].dirty =
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0x40) >> 6 != 0;
(device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0x40) >> 6 != 0;
device.memory.dcache[dcache_line].tag =
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG as usize] & 0xFFFFF00) << 4)
as u32
((device.cpu.cop0.regs[device::cop0::COP0_TAGLO_REG] & 0xFFFFF00) << 4) as u32
}
0x0D => {
//dcache create dirty exclusive
@@ -765,7 +763,7 @@ pub fn cache(device: &mut device::Device, opcode: u32) {
}
}
_ => {
panic!("unknown cache code {:#01x}", rt(opcode))
panic!("unknown cache code {:x}", rt(opcode))
}
}
}
@@ -787,7 +785,7 @@ pub fn ll(device: &mut device::Device, opcode: u32) {
cached,
) as i32);
device.cpu.llbit = true;
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG as usize] = phys_address >> 4
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG] = phys_address >> 4
}
pub fn lld(device: &mut device::Device, opcode: u32) {
@@ -816,7 +814,7 @@ pub fn lld(device: &mut device::Device, opcode: u32) {
device.cpu.gpr[rt(opcode) as usize] = ((w[0] as u64) << 32) | (w[1]) as u64;
device.cpu.llbit = true;
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG as usize] = phys_address >> 4
device.cpu.cop0.regs[device::cop0::COP0_LLADDR_REG] = phys_address >> 4
}
pub fn ld(device: &mut device::Device, opcode: u32) {
+4 -2
View File
@@ -15,7 +15,8 @@ pub const EVENT_TYPE_VRU: usize = 10;
pub const EVENT_TYPE_PAK: usize = 11;
pub const EVENT_TYPE_SAVE: usize = 12;
pub const EVENT_TYPE_NMI: usize = 13;
pub const EVENT_TYPE_COUNT: usize = 14;
pub const EVENT_TYPE_UPDATE_SCREEN: usize = 14;
pub const EVENT_TYPE_COUNT: usize = 15;
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct Event {
@@ -26,7 +27,7 @@ pub struct Event {
pub fn create_event(device: &mut device::Device, name: usize, when: u64) {
device.cpu.events[name] = Event {
enabled: true,
count: device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize] + when,
count: device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] + when,
};
set_next_event(device);
}
@@ -65,6 +66,7 @@ fn get_event_handler(name: usize) -> fn(&mut device::Device) {
EVENT_TYPE_PAK => device::controller::pak_switch_event,
EVENT_TYPE_SAVE => ui::storage::save_event,
EVENT_TYPE_NMI => device::exceptions::reset_event,
EVENT_TYPE_UPDATE_SCREEN => device::vi::update_screen,
_ => dummy_event,
}
}
+35 -53
View File
@@ -2,7 +2,7 @@ use crate::device;
use crate::ui;
pub fn check_pending_interrupts(device: &mut device::Device) {
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
& (device::cop0::COP0_STATUS_IE
| device::cop0::COP0_STATUS_EXL
| device::cop0::COP0_STATUS_ERL))
@@ -12,17 +12,14 @@ pub fn check_pending_interrupts(device: &mut device::Device) {
return;
}
if device.mi.regs[device::mi::MI_INTR_REG as usize]
& device.mi.regs[device::mi::MI_INTR_MASK_REG as usize]
!= 0
{
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |= device::cop0::COP0_CAUSE_IP2;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
if device.mi.regs[device::mi::MI_INTR_REG] & device.mi.regs[device::mi::MI_INTR_MASK_REG] != 0 {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP2;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &=
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
}
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
& device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize]
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
& device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG]
& device::cop0::COP0_CAUSE_IP_MASK)
== 0
{
@@ -42,43 +39,37 @@ pub fn interrupt_exception(device: &mut device::Device) {
}
pub fn floating_point_exception(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_FPE;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_FPE;
exception_general(device, 0x180)
}
pub fn trap_exception(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_TR;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TR;
exception_general(device, 0x180)
}
pub fn syscall_exception(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_SYS;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_SYS;
exception_general(device, 0x180)
}
pub fn break_exception(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_BP;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_BP;
exception_general(device, 0x180)
}
pub fn reserved_exception(device: &mut device::Device, cop: u64) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_RI | cop;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_RI | cop;
exception_general(device, 0x180)
}
pub fn cop_unusable_exception(device: &mut device::Device, cop: u64) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_CPU | cop;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_CPU | cop;
exception_general(device, 0x180)
}
@@ -89,31 +80,29 @@ pub fn tlb_miss_exception(
access_type: device::memory::AccessType,
) {
if access_type == device::memory::AccessType::Read {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_TLBL
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBL
} else {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device::cop0::COP0_CAUSE_EXCCODE_TLBS
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBS
}
device.cpu.cop0.regs[device::cop0::COP0_BADVADDR_REG as usize] = address;
device.cpu.cop0.regs[device::cop0::COP0_BADVADDR_REG] = address;
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_CONTEXT_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_CONTEXT_REG],
address >> 9,
device::cop0::COP0_CONTEXT_BADVPN2_MASK,
);
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
address >> 9,
device::cop0::COP0_XCONTEXT_BADVPN2_MASK,
);
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
address >> 31,
device::cop0::COP0_XCONTEXT_REGION_MASK,
);
device::memory::masked_write_64(
&mut device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize],
&mut device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG],
address,
0xFFFFE000,
);
@@ -124,7 +113,7 @@ pub fn tlb_miss_exception(
if address & !3 >= i.start_even && address & !3 <= i.end_even {
valid = i.v_even != 0;
if valid && access_type == device::memory::AccessType::Write && i.d_even == 0 {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
device::cop0::COP0_CAUSE_EXCCODE_MOD;
valid = false;
}
@@ -133,7 +122,7 @@ pub fn tlb_miss_exception(
if address & !3 >= i.start_odd && address & !3 <= i.end_odd {
valid = i.v_odd != 0;
if valid && access_type == device::memory::AccessType::Write && i.d_odd == 0 {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] =
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
device::cop0::COP0_CAUSE_EXCCODE_MOD;
valid = false;
}
@@ -141,8 +130,7 @@ pub fn tlb_miss_exception(
}
}
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_EXL
== 0
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0
&& valid
{
vector_offset = 0;
@@ -152,43 +140,37 @@ pub fn tlb_miss_exception(
}
pub fn reset_event(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &= !device::cop0::COP0_CAUSE_IP4;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP4;
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_ERL
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= device::cop0::COP0_STATUS_ERL
| device::cop0::COP0_STATUS_SR
| device::cop0::COP0_STATUS_BEV;
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] &= !device::cop0::COP0_STATUS_TS;
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] &= !device::cop0::COP0_STATUS_TS;
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG as usize] = device.cpu.pc;
device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG] = device.cpu.pc;
device.cpu.pc = 0xBFC00000;
device.cpu.branch_state.state = device::cpu::State::Step;
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] = 0;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] = 0;
device::pif::reset_pif(device, true);
ui::video::onscreen_message("Game reset", false);
ui::video::onscreen_message("Game reset", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
}
fn exception_general(device: &mut device::Device, vector_offset: u32) {
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_EXL
== 0
{
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG as usize] = device.cpu.pc;
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0 {
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] = device.cpu.pc;
if device::cpu::in_delay_slot(device) {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] |=
device::cop0::COP0_CAUSE_BD;
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG as usize] -= 4;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_BD;
device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] -= 4;
} else {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
!device::cop0::COP0_CAUSE_BD;
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_BD;
}
}
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] |= device::cop0::COP0_STATUS_EXL;
device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= device::cop0::COP0_STATUS_EXL;
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize] & device::cop0::COP0_STATUS_BEV
== 0
{
if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_BEV == 0 {
device.cpu.pc = device::cpu_instructions::se32((0x80000000 + vector_offset) as i32);
} else {
device.cpu.pc = device::cpu_instructions::se32((0xBFC00200 + vector_offset) as i32);
+1 -1
View File
@@ -38,7 +38,7 @@ pub enum AccessSize {
Icache = 32,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Memory {
#[serde(skip, default = "savestates::default_memory_read_fast")]
pub fast_read: [fn(&device::Device, u64, AccessSize) -> u32; 0x2000], // fast_read is used for lookups that try to detect idle loops
+35 -37
View File
@@ -1,10 +1,10 @@
use crate::device;
pub const MI_INIT_MODE_REG: u32 = 0;
const MI_VERSION_REG: u32 = 1;
pub const MI_INTR_REG: u32 = 2;
pub const MI_INTR_MASK_REG: u32 = 3;
pub const MI_REGS_COUNT: u32 = 4;
pub const MI_INIT_MODE_REG: usize = 0;
const MI_VERSION_REG: usize = 1;
pub const MI_INTR_REG: usize = 2;
pub const MI_INTR_MASK_REG: usize = 3;
pub const MI_REGS_COUNT: usize = 4;
/* read */
pub const MI_INTR_SP: u32 = 1 << 0;
@@ -44,9 +44,9 @@ const MI_SET_RDRAM: u32 = 1 << 13;
pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Mi {
pub regs: [u32; MI_REGS_COUNT as usize],
pub regs: [u32; MI_REGS_COUNT],
}
pub fn read_regs(
@@ -60,46 +60,45 @@ pub fn read_regs(
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
MI_INIT_MODE_REG => update_init_mode(device, value),
MI_INTR_MASK_REG => update_intr_mask(device, value),
_ => device::memory::masked_write_32(&mut device.mi.regs[reg as usize], value, mask),
}
if device.mi.regs[MI_INTR_REG as usize] & device.mi.regs[MI_INTR_MASK_REG as usize] == 0 {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
!device::cop0::COP0_CAUSE_IP2;
if device.mi.regs[MI_INTR_REG] & device.mi.regs[MI_INTR_MASK_REG] == 0 {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP2;
}
device::exceptions::check_pending_interrupts(device)
}
fn update_init_mode(device: &mut device::Device, w: u32) {
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_LENGTH_MASK;
device.mi.regs[MI_INIT_MODE_REG as usize] |= w & MI_INIT_LENGTH_MASK;
device.mi.regs[MI_INIT_MODE_REG] &= !MI_INIT_LENGTH_MASK;
device.mi.regs[MI_INIT_MODE_REG] |= w & MI_INIT_LENGTH_MASK;
if w & MI_CLR_INIT != 0 {
for i in 0..(0x3F00000 >> 16) {
device.memory.memory_map_write[i] = device::rdram::write_mem;
}
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_INIT_MODE
device.mi.regs[MI_INIT_MODE_REG] &= !MI_INIT_MODE
}
if w & MI_SET_INIT != 0 {
for i in 0..(0x3F00000 >> 16) {
device.memory.memory_map_write[i] = device::rdram::write_mem_repeat;
}
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_INIT_MODE
device.mi.regs[MI_INIT_MODE_REG] |= MI_INIT_MODE
}
if w & MI_CLR_EBUS != 0 {
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_EBUS_MODE
device.mi.regs[MI_INIT_MODE_REG] &= !MI_EBUS_MODE
}
if w & MI_SET_EBUS != 0 {
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_EBUS_MODE
device.mi.regs[MI_INIT_MODE_REG] |= MI_EBUS_MODE
}
if w & MI_CLR_RDRAM != 0 {
device.mi.regs[MI_INIT_MODE_REG as usize] &= !MI_RDRAM_MODE
device.mi.regs[MI_INIT_MODE_REG] &= !MI_RDRAM_MODE
}
if w & MI_SET_RDRAM != 0 {
device.mi.regs[MI_INIT_MODE_REG as usize] |= MI_RDRAM_MODE
device.mi.regs[MI_INIT_MODE_REG] |= MI_RDRAM_MODE
}
if w & MI_CLR_DP_INTR != 0 {
@@ -109,57 +108,56 @@ fn update_init_mode(device: &mut device::Device, w: u32) {
fn update_intr_mask(device: &mut device::Device, w: u32) {
if w & MI_CLR_SP != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_SP
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_SP
}
if w & MI_SET_SP != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_SP
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_SP
}
if w & MI_CLR_SI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_SI
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_SI
}
if w & MI_SET_SI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_SI
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_SI
}
if w & MI_CLR_AI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_AI
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_AI
}
if w & MI_SET_AI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_AI
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_AI
}
if w & MI_CLR_VI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_VI
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_VI
}
if w & MI_SET_VI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_VI
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_VI
}
if w & MI_CLR_PI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_PI
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_PI
}
if w & MI_SET_PI != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_PI
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_PI
}
if w & MI_CLR_DP != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] &= !MI_INTR_DP
device.mi.regs[MI_INTR_MASK_REG] &= !MI_INTR_DP
}
if w & MI_SET_DP != 0 {
device.mi.regs[MI_INTR_MASK_REG as usize] |= MI_INTR_DP
device.mi.regs[MI_INTR_MASK_REG] |= MI_INTR_DP
}
}
pub fn clear_rcp_interrupt(device: &mut device::Device, interrupt: u32) {
device.mi.regs[MI_INTR_REG as usize] &= !interrupt;
device.mi.regs[MI_INTR_REG] &= !interrupt;
if device.mi.regs[MI_INTR_REG as usize] & device.mi.regs[MI_INTR_MASK_REG as usize] == 0 {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] &=
!device::cop0::COP0_CAUSE_IP2;
if device.mi.regs[MI_INTR_REG] & device.mi.regs[MI_INTR_MASK_REG] == 0 {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP2;
}
}
pub fn set_rcp_interrupt(device: &mut device::Device, interrupt: u32) {
device.mi.regs[MI_INTR_REG as usize] |= interrupt;
device.mi.regs[MI_INTR_REG] |= interrupt;
device::exceptions::check_pending_interrupts(device)
}
pub fn init(device: &mut device::Device) {
device.mi.regs[MI_VERSION_REG as usize] = 0x02020102
device.mi.regs[MI_VERSION_REG] = 0x02020102
}
+44 -48
View File
@@ -2,21 +2,21 @@ use crate::device;
use crate::ui;
use rand::Rng;
const PI_DRAM_ADDR_REG: u32 = 0;
const PI_CART_ADDR_REG: u32 = 1;
const PI_RD_LEN_REG: u32 = 2;
const PI_WR_LEN_REG: u32 = 3;
pub const PI_STATUS_REG: u32 = 4;
const PI_BSD_DOM1_LAT_REG: u32 = 5;
const PI_BSD_DOM1_PWD_REG: u32 = 6;
const PI_BSD_DOM1_PGS_REG: u32 = 7;
const PI_BSD_DOM1_RLS_REG: u32 = 8;
const PI_BSD_DOM2_LAT_REG: u32 = 9;
const PI_BSD_DOM2_PWD_REG: u32 = 10;
const PI_BSD_DOM2_PGS_REG: u32 = 11;
const PI_BSD_DOM2_RLS_REG: u32 = 12;
//const UNKNOWN_REG: u32 = 13; //LibDragon
pub const PI_REGS_COUNT: u32 = 14;
const PI_DRAM_ADDR_REG: usize = 0;
const PI_CART_ADDR_REG: usize = 1;
const PI_RD_LEN_REG: usize = 2;
const PI_WR_LEN_REG: usize = 3;
pub const PI_STATUS_REG: usize = 4;
const PI_BSD_DOM1_LAT_REG: usize = 5;
const PI_BSD_DOM1_PWD_REG: usize = 6;
const PI_BSD_DOM1_PGS_REG: usize = 7;
const PI_BSD_DOM1_RLS_REG: usize = 8;
const PI_BSD_DOM2_LAT_REG: usize = 9;
const PI_BSD_DOM2_PWD_REG: usize = 10;
const PI_BSD_DOM2_PGS_REG: usize = 11;
const PI_BSD_DOM2_RLS_REG: usize = 12;
//const UNKNOWN_REG: usize = 13; //LibDragon
pub const PI_REGS_COUNT: usize = 14;
/* PI_STATUS - read */
const PI_STATUS_DMA_BUSY: u32 = 1 << 0;
@@ -28,9 +28,9 @@ const PI_STATUS_INTERRUPT: u32 = 1 << 3;
const PI_STATUS_RESET: u32 = 1 << 0;
const PI_STATUS_CLR_INTR: u32 = 1 << 1;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Pi {
pub regs: [u32; PI_REGS_COUNT as usize],
pub regs: [u32; PI_REGS_COUNT],
}
struct PiHandler {
@@ -45,7 +45,7 @@ pub fn read_regs(
) -> u32 {
device::cop0::add_cycles(device, 20);
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
PI_WR_LEN_REG | PI_RD_LEN_REG => 0x7F,
PI_CART_ADDR_REG => device.pi.regs[reg as usize] & 0xFFFFFFFE,
PI_DRAM_ADDR_REG => device.pi.regs[reg as usize] & 0xFFFFFE,
@@ -54,11 +54,11 @@ pub fn read_regs(
}
fn dma_read(device: &mut device::Device) {
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG as usize]);
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG]);
let cart_addr = device.pi.regs[PI_CART_ADDR_REG as usize] & !1;
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG as usize] & 0xFFFFFE;
let mut length = (device.pi.regs[PI_RD_LEN_REG as usize] & 0xFFFFFF) + 1;
let cart_addr = device.pi.regs[PI_CART_ADDR_REG] & !1;
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG] & 0xFFFFFE;
let mut length = (device.pi.regs[PI_RD_LEN_REG] & 0xFFFFFF) + 1;
/* PI seems to treat the first 128 bytes differently, see https://n64brew.dev/wiki/Peripheral_Interface#Unaligned_DMA_transfer */
if length >= 0x7f && (length & 1) != 0 {
@@ -70,20 +70,18 @@ fn dma_read(device: &mut device::Device) {
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
/* Update PI_DRAM_ADDR_REG and PI_CART_ADDR_REG */
device.pi.regs[PI_DRAM_ADDR_REG as usize] =
(device.pi.regs[PI_DRAM_ADDR_REG as usize] + length + 7) & !7;
device.pi.regs[PI_CART_ADDR_REG as usize] =
(device.pi.regs[PI_CART_ADDR_REG as usize] + length + 1) & !1;
device.pi.regs[PI_DRAM_ADDR_REG] = (device.pi.regs[PI_DRAM_ADDR_REG] + length + 7) & !7;
device.pi.regs[PI_CART_ADDR_REG] = (device.pi.regs[PI_CART_ADDR_REG] + length + 1) & !1;
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_DMA_BUSY
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_DMA_BUSY
}
fn dma_write(device: &mut device::Device) {
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG as usize]);
let handler = get_handler(device.pi.regs[PI_CART_ADDR_REG]);
let cart_addr = device.pi.regs[PI_CART_ADDR_REG as usize] & !1;
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG as usize] & 0xFFFFFE;
let mut length = (device.pi.regs[PI_WR_LEN_REG as usize] & 0xFFFFFF) + 1;
let cart_addr = device.pi.regs[PI_CART_ADDR_REG] & !1;
let dram_addr = device.pi.regs[PI_DRAM_ADDR_REG] & 0xFFFFFE;
let mut length = (device.pi.regs[PI_WR_LEN_REG] & 0xFFFFFF) + 1;
/* PI seems to treat the first 128 bytes differently, see https://n64brew.dev/wiki/Peripheral_Interface#Unaligned_DMA_transfer */
if length >= 0x7f && (length & 1) != 0 {
@@ -101,12 +99,10 @@ fn dma_write(device: &mut device::Device) {
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
/* Update PI_DRAM_ADDR_REG and PI_CART_ADDR_REG */
device.pi.regs[PI_DRAM_ADDR_REG as usize] =
(device.pi.regs[PI_DRAM_ADDR_REG as usize] + length + 7) & !7;
device.pi.regs[PI_CART_ADDR_REG as usize] =
(device.pi.regs[PI_CART_ADDR_REG as usize] + length + 1) & !1;
device.pi.regs[PI_DRAM_ADDR_REG] = (device.pi.regs[PI_DRAM_ADDR_REG] + length + 7) & !7;
device.pi.regs[PI_CART_ADDR_REG] = (device.pi.regs[PI_CART_ADDR_REG] + length + 1) & !1;
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_DMA_BUSY
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_DMA_BUSY
}
fn get_handler(address: u32) -> PiHandler {
@@ -138,7 +134,7 @@ fn get_handler(address: u32) -> PiHandler {
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
PI_RD_LEN_REG => {
device::memory::masked_write_32(&mut device.pi.regs[reg as usize], value, mask);
device::pi::dma_read(device)
@@ -153,7 +149,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_PI);
}
if value & mask & PI_STATUS_RESET != 0 {
device.pi.regs[PI_STATUS_REG as usize] = 0
device.pi.regs[PI_STATUS_REG] = 0
}
}
PI_BSD_DOM1_LAT_REG | PI_BSD_DOM1_PWD_REG | PI_BSD_DOM2_LAT_REG | PI_BSD_DOM2_PWD_REG => {
@@ -179,15 +175,15 @@ pub fn calculate_cycles(device: &mut device::Device, domain: i32, length: u32) -
let page_size_base: f64 = 2.0;
if domain == 1 {
latency = (device.pi.regs[PI_BSD_DOM1_LAT_REG as usize] + 1) as f64;
pulse_width = (device.pi.regs[PI_BSD_DOM1_PWD_REG as usize] + 1) as f64;
release = (device.pi.regs[PI_BSD_DOM1_RLS_REG as usize] + 1) as f64;
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM1_PGS_REG as usize] + 2) as f64);
latency = (device.pi.regs[PI_BSD_DOM1_LAT_REG] + 1) as f64;
pulse_width = (device.pi.regs[PI_BSD_DOM1_PWD_REG] + 1) as f64;
release = (device.pi.regs[PI_BSD_DOM1_RLS_REG] + 1) as f64;
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM1_PGS_REG] + 2) as f64);
} else if domain == 2 {
latency = (device.pi.regs[PI_BSD_DOM2_LAT_REG as usize] + 1) as f64;
pulse_width = (device.pi.regs[PI_BSD_DOM2_PWD_REG as usize] + 1) as f64;
release = (device.pi.regs[PI_BSD_DOM2_RLS_REG as usize] + 1) as f64;
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM2_PGS_REG as usize] + 2) as f64);
latency = (device.pi.regs[PI_BSD_DOM2_LAT_REG] + 1) as f64;
pulse_width = (device.pi.regs[PI_BSD_DOM2_PWD_REG] + 1) as f64;
release = (device.pi.regs[PI_BSD_DOM2_RLS_REG] + 1) as f64;
page_size = page_size_base.powf((device.pi.regs[PI_BSD_DOM2_PGS_REG] + 2) as f64);
} else {
panic!("unknown pi dma")
}
@@ -201,8 +197,8 @@ pub fn calculate_cycles(device: &mut device::Device, domain: i32, length: u32) -
}
pub fn dma_event(device: &mut device::Device) {
device.pi.regs[PI_STATUS_REG as usize] &= !(PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY);
device.pi.regs[PI_STATUS_REG as usize] |= PI_STATUS_INTERRUPT;
device.pi.regs[PI_STATUS_REG] &= !(PI_STATUS_DMA_BUSY | PI_STATUS_IO_BUSY);
device.pi.regs[PI_STATUS_REG] |= PI_STATUS_INTERRUPT;
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_PI)
}
+4 -4
View File
@@ -1,7 +1,7 @@
mod rom;
use crate::device;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Pif {
#[serde(with = "serde_big_array::BigArray")]
pub rom: [u8; 1984],
@@ -68,7 +68,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
device.si.dma_dir = device::si::DmaDir::Write;
device::events::create_event(device, device::events::EVENT_TYPE_SI, 3200); //based on https://github.com/rasky/n64-systembench
device.si.regs[device::si::SI_STATUS_REG as usize] |=
device.si.regs[device::si::SI_STATUS_REG] |=
device::si::SI_STATUS_DMA_BUSY | device::si::SI_STATUS_IO_BUSY
}
@@ -219,7 +219,7 @@ pub fn process_ram(device: &mut device::Device) {
pub fn connect_pif_channels(device: &mut device::Device) {
for i in 0..4 {
if let Some(netplay) = &device.netplay {
if netplay.player_data[i].reg_id != 0 {
if netplay.connected[i] {
device.pif.channels[i].process = Some(device::controller::process);
}
} else if device.ui.config.input.controller_enabled[i] {
@@ -278,7 +278,7 @@ pub fn init(device: &mut device::Device) {
for i in 0..4 {
if let Some(netplay) = &device.netplay {
if netplay.player_data[i].reg_id != 0 {
if netplay.connected[i] {
device.pif.channels[i].pak_handler = Some(default_handler);
}
} else if device.ui.config.input.controller_enabled[i] {
+44 -54
View File
@@ -1,20 +1,20 @@
use crate::{device, savestates, ui};
use crate::{device, ui};
pub const DPC_START_REG: u32 = 0;
pub const DPC_END_REG: u32 = 1;
pub const DPC_CURRENT_REG: u32 = 2;
pub const DPC_STATUS_REG: u32 = 3;
const DPC_CLOCK_REG: u32 = 4;
const DPC_BUFBUSY_REG: u32 = 5;
const DPC_PIPEBUSY_REG: u32 = 6;
const DPC_TMEM_REG: u32 = 7;
pub const DPC_REGS_COUNT: u32 = 8;
pub const DPC_START_REG: usize = 0;
pub const DPC_END_REG: usize = 1;
pub const DPC_CURRENT_REG: usize = 2;
pub const DPC_STATUS_REG: usize = 3;
const DPC_CLOCK_REG: usize = 4;
const DPC_BUFBUSY_REG: usize = 5;
const DPC_PIPEBUSY_REG: usize = 6;
const DPC_TMEM_REG: usize = 7;
pub const DPC_REGS_COUNT: usize = 8;
//const DPS_TBIST_REG: u32 = 0;
//const DPS_TEST_MODE_REG: u32 = 1;
//const DPS_BUFTEST_ADDR_REG: u32 = 2;
//const DPS_BUFTEST_DATA_REG: u32 = 3;
pub const DPS_REGS_COUNT: u32 = 4;
//const DPS_TBIST_REG: usize = 0;
//const DPS_TEST_MODE_REG: usize = 1;
//const DPS_BUFTEST_ADDR_REG: usize = 2;
//const DPS_BUFTEST_DATA_REG: usize = 3;
pub const DPS_REGS_COUNT: usize = 4;
/* DPC status - read */
const DPC_STATUS_XBUS_DMEM_DMA: u32 = 1 << 0;
@@ -40,10 +40,10 @@ const DPC_CLR_PIPE_CTR: u32 = 1 << 7;
const DPC_CLR_CMD_CTR: u32 = 1 << 8;
const DPC_CLR_CLOCK_CTR: u32 = 1 << 9;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Rdp {
pub regs_dpc: [u32; DPC_REGS_COUNT as usize],
pub regs_dps: [u32; DPS_REGS_COUNT as usize],
pub regs_dpc: [u32; DPC_REGS_COUNT],
pub regs_dps: [u32; DPS_REGS_COUNT],
pub wait_frozen: bool,
pub last_status_value: u32,
}
@@ -57,7 +57,7 @@ pub fn read_regs_dpc(
device::cop0::add_cycles(device, 20);
}
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
DPC_STATUS_REG => {
let value =
device.rdp.regs_dpc[reg as usize] & (DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY);
@@ -80,18 +80,18 @@ pub fn read_regs_dpc(
pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
DPC_CURRENT_REG | DPC_CLOCK_REG | DPC_BUFBUSY_REG | DPC_PIPEBUSY_REG | DPC_TMEM_REG => {}
DPC_STATUS_REG => update_dpc_status(device, value & mask),
DPC_START_REG => {
if (device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_START_VALID) == 0 {
if (device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_START_VALID) == 0 {
device::memory::masked_write_32(
&mut device.rdp.regs_dpc[reg as usize],
value & 0xFFFFF8,
mask,
)
}
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_START_VALID
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_START_VALID
}
DPC_END_REG => {
device::memory::masked_write_32(
@@ -99,12 +99,11 @@ pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mas
value & 0xFFFFF8,
mask,
);
if (device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_START_VALID) != 0 {
device.rdp.regs_dpc[DPC_CURRENT_REG as usize] =
device.rdp.regs_dpc[DPC_START_REG as usize];
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_START_VALID
if (device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_START_VALID) != 0 {
device.rdp.regs_dpc[DPC_CURRENT_REG] = device.rdp.regs_dpc[DPC_START_REG];
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_START_VALID
}
if device.rdp.regs_dpc[DPC_STATUS_REG as usize] & DPC_STATUS_FREEZE == 0 {
if device.rdp.regs_dpc[DPC_STATUS_REG] & DPC_STATUS_FREEZE == 0 {
run_rdp(device)
} else {
device.rdp.wait_frozen = true;
@@ -116,20 +115,11 @@ pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mas
fn run_rdp(device: &mut device::Device) {
let timer = ui::video::process_rdp_list();
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |=
device.rdp.regs_dpc[DPC_STATUS_REG] |=
DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY;
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0xFFFFFF;
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF;
if timer != 0 {
if device.save_state {
// Right after full sync, good time for save state
device.save_state = false;
savestates::create_savestate(device);
} else if device.load_state {
// Right after full sync, good time for save state
device.load_state = false;
savestates::load_savestate(device);
}
device::events::create_event(device, device::events::EVENT_TYPE_DP, timer)
}
}
@@ -154,59 +144,59 @@ pub fn write_regs_dps(device: &mut device::Device, address: u64, value: u32, mas
fn update_dpc_status(device: &mut device::Device, w: u32) {
/* clear / set xbus_dmem_dma */
if w & DPC_CLR_XBUS_DMEM_DMA != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_XBUS_DMEM_DMA
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_XBUS_DMEM_DMA
}
if w & DPC_SET_XBUS_DMEM_DMA != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_XBUS_DMEM_DMA
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_XBUS_DMEM_DMA
}
/* clear / set freeze */
if w & DPC_CLR_FREEZE != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_FREEZE;
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_FREEZE;
if device.rdp.wait_frozen {
run_rdp(device);
device.rdp.wait_frozen = false;
}
}
if w & DPC_SET_FREEZE != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_FREEZE;
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_FREEZE;
}
/* clear / set flush */
if w & DPC_CLR_FLUSH != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_FLUSH
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_FLUSH
}
if w & DPC_SET_FLUSH != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |= DPC_STATUS_FLUSH
device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_FLUSH
}
if w & DPC_CLR_TMEM_CTR != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_TMEM_BUSY;
device.rdp.regs_dpc[DPC_TMEM_REG as usize] = 0
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_TMEM_BUSY;
device.rdp.regs_dpc[DPC_TMEM_REG] = 0
}
if w & DPC_CLR_PIPE_CTR != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_PIPE_BUSY;
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_PIPE_BUSY;
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0
}
if w & DPC_CLR_CMD_CTR != 0 {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &= !DPC_STATUS_CMD_BUSY;
device.rdp.regs_dpc[DPC_BUFBUSY_REG as usize] = 0
device.rdp.regs_dpc[DPC_STATUS_REG] &= !DPC_STATUS_CMD_BUSY;
device.rdp.regs_dpc[DPC_BUFBUSY_REG] = 0
}
/* clear clock counter */
if w & DPC_CLR_CLOCK_CTR != 0 {
device.rdp.regs_dpc[DPC_CLOCK_REG as usize] = 0
device.rdp.regs_dpc[DPC_CLOCK_REG] = 0
}
}
pub fn init(device: &mut device::Device) {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] |=
device.rdp.regs_dpc[DPC_STATUS_REG] |=
DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CBUF_READY;
device.rdp.regs_dpc[DPC_PIPEBUSY_REG as usize] = 0xFFFFFF;
device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF;
}
pub fn rdp_interrupt_event(device: &mut device::Device) {
device.rdp.regs_dpc[DPC_STATUS_REG as usize] &=
device.rdp.regs_dpc[DPC_STATUS_REG] &=
!(DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY);
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_DP)
+20 -21
View File
@@ -3,25 +3,25 @@ use crate::retroachievements;
use crate::ui;
use std::alloc::{Layout, alloc_zeroed};
//const RDRAM_CONFIG_REG: u32 = 0;
//const RDRAM_DEVICE_ID_REG: u32 = 1;
//const RDRAM_DELAY_REG: u32 = 2;
const RDRAM_MODE_REG: u32 = 3;
//const RDRAM_REF_INTERVAL_REG: u32 = 4;
//const RDRAM_REF_ROW_REG: u32 = 5;
//const RDRAM_RAS_INTERVAL_REG: u32 = 6;
//const RDRAM_MIN_INTERVAL_REG: u32 = 7;
//const RDRAM_ADDR_SELECT_REG: u32 = 8;
//const RDRAM_DEVICE_MANUF_REG: u32 = 9;
pub const RDRAM_REGS_COUNT: u32 = 10;
//const RDRAM_CONFIG_REG: usize = 0;
//const RDRAM_DEVICE_ID_REG: usize = 1;
//const RDRAM_DELAY_REG: usize = 2;
const RDRAM_MODE_REG: usize = 3;
//const RDRAM_REF_INTERVAL_REG: usize = 4;
//const RDRAM_REF_ROW_REG: usize = 5;
//const RDRAM_RAS_INTERVAL_REG: usize = 6;
//const RDRAM_MIN_INTERVAL_REG: usize = 7;
//const RDRAM_ADDR_SELECT_REG: usize = 8;
//const RDRAM_DEVICE_MANUF_REG: usize = 9;
pub const RDRAM_REGS_COUNT: usize = 10;
pub const RDRAM_MASK: usize = 0xFFFFFF;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Rdram {
pub mem: Vec<u8>,
pub size: u32,
pub regs: [[u32; RDRAM_REGS_COUNT as usize]; 4],
pub regs: [[u32; RDRAM_REGS_COUNT]; 4],
}
pub fn read_mem_fast(
@@ -91,9 +91,8 @@ pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, m
panic!("RDRAM write_mem_repeat called with mask {:#x}", mask);
}
let repeat_length = (device.mi.regs[device::mi::MI_INIT_MODE_REG as usize]
& device::mi::MI_INIT_LENGTH_MASK)
+ 1;
let repeat_length =
(device.mi.regs[device::mi::MI_INIT_MODE_REG] & device::mi::MI_INIT_LENGTH_MASK) + 1;
if !repeat_length.is_multiple_of(4) {
panic!(
@@ -113,7 +112,7 @@ pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, m
.copy_from_slice(&value.to_ne_bytes());
}
device.mi.regs[device::mi::MI_INIT_MODE_REG as usize] &= !device::mi::MI_INIT_MODE;
device.mi.regs[device::mi::MI_INIT_MODE_REG] &= !device::mi::MI_INIT_MODE;
for i in 0..(0x3F00000 >> 16) {
device.memory.memory_map_write[i] = device::rdram::write_mem;
}
@@ -127,7 +126,7 @@ pub fn read_regs(
device::cop0::add_cycles(device, 20);
let chip_id = (address >> 13) & 3;
let reg = (address & 0x3FF) >> 2;
match reg as u32 {
match reg as usize {
RDRAM_MODE_REG => device.pi.regs[reg as usize] ^ 0xc0c0c0c0,
0x80 => 0x00000000, //Row, needed for libdragon
_ => device.rdram.regs[chip_id as usize][reg as usize],
@@ -152,7 +151,7 @@ pub fn init(device: &mut device::Device) {
device.rdram.mem =
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
retroachievements::set_dmem(device.rdram.mem.as_ptr(), device.rdram.size as usize);
retroachievements::set_rdram(device.rdram.mem.as_ptr(), device.rdram.size as usize);
// hack, skip RDRAM initialization
device
@@ -169,8 +168,8 @@ pub fn init(device: &mut device::Device) {
.unwrap_or(&mut [0; 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_MODE_REG] = 0x0e;
device.ri.regs[device::ri::RI_CONFIG_REG] = 0x40;
}
pub fn rdram_calculate_cycles(length: u64) -> u64 {
+13 -13
View File
@@ -1,18 +1,18 @@
use crate::device;
pub const RI_MODE_REG: u32 = 0;
pub const RI_CONFIG_REG: u32 = 1;
//const RI_CURRENT_LOAD_REG: u32 = 2;
pub const RI_SELECT_REG: u32 = 3;
pub const RI_REFRESH_REG: u32 = 4;
//const RI_LATENCY_REG: u32 = 5;
//const RI_ERROR_REG: u32 = 6;
//const RI_WERROR_REG: u32 = 7;
pub const RI_REGS_COUNT: u32 = 8;
pub const RI_MODE_REG: usize = 0;
pub const RI_CONFIG_REG: usize = 1;
//const RI_CURRENT_LOAD_REG: usize = 2;
pub const RI_SELECT_REG: usize = 3;
pub const RI_REFRESH_REG: usize = 4;
//const RI_LATENCY_REG: usize = 5;
//const RI_ERROR_REG: usize = 6;
//const RI_WERROR_REG: usize = 7;
pub const RI_REGS_COUNT: usize = 8;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Ri {
pub regs: [u32; RI_REGS_COUNT as usize],
pub regs: [u32; RI_REGS_COUNT],
pub ram_init: bool,
}
@@ -23,7 +23,7 @@ pub fn read_regs(
) -> u32 {
device::cop0::add_cycles(device, 20);
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
RI_SELECT_REG => {
if !device.ri.ram_init {
device::cop0::add_cycles(device, device.cpu.clock_rate / 2); // hack, simulate RDRAM initialization
@@ -38,7 +38,7 @@ pub fn read_regs(
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
if reg as u32 == RI_SELECT_REG {
if reg as usize == RI_SELECT_REG {
device.ri.ram_init = false;
}
device::memory::masked_write_32(&mut device.ri.regs[reg as usize], value, mask);
+10 -11
View File
@@ -6,7 +6,7 @@ use crate::{device, savestates};
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct BranchState {
pub state: device::cpu::State,
pub pc: u32,
@@ -25,7 +25,7 @@ pub enum InstructionType {
Vu,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cpu {
#[serde(with = "serde_big_array::BigArray")]
pub instructions: [Instructions; 0x1000 / 4],
@@ -132,48 +132,47 @@ pub fn run(device: &mut device::Device) -> u64 {
device.rsp.cpu.gpr[0] = 0; // gpr 0 is read only
let instruction = device.rsp.cpu.instructions
[(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] / 4) as usize];
[(device.rsp.regs2[device::rsp_interface::SP_PC_REG] / 4) as usize];
(instruction.func)(device, instruction.opcode);
match device.rsp.cpu.branch_state.state {
device::cpu::State::Step => {
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
if device.rsp.cpu.broken {
break;
}
}
device::cpu::State::Take => {
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
device.rsp.cpu.branch_state.state = device::cpu::State::DelaySlotTaken
}
device::cpu::State::NotTaken => {
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
device.rsp.cpu.branch_state.state = device::cpu::State::DelaySlotNotTaken
}
device::cpu::State::DelaySlotTaken => {
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] =
device.rsp.cpu.branch_state.pc;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] = device.rsp.cpu.branch_state.pc;
device.rsp.cpu.branch_state.state = device::cpu::State::Step;
if device.rsp.cpu.broken {
break;
}
}
device::cpu::State::DelaySlotNotTaken => {
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 4;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 4;
device.rsp.cpu.branch_state.state = device::cpu::State::Step;
if device.rsp.cpu.broken {
break;
}
}
device::cpu::State::Discard => {
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] += 8;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] += 8;
device.rsp.cpu.branch_state.state = device::cpu::State::Step
}
device::cpu::State::Exception => {
device.rsp.cpu.branch_state.state = device::cpu::State::Step
}
}
device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] &= 0xFFC;
device.rsp.regs2[device::rsp_interface::SP_PC_REG] &= 0xFFC;
if device.rsp.cpu.instruction_type == device.rsp.cpu.last_instruction_type {
device.rsp.cpu.cycle_counter += 1;
+71 -71
View File
@@ -1,19 +1,19 @@
use crate::device;
use crate::ui;
const SP_MEM_ADDR_REG: u32 = 0;
const SP_DRAM_ADDR_REG: u32 = 1;
const SP_RD_LEN_REG: u32 = 2;
const SP_WR_LEN_REG: u32 = 3;
pub const SP_STATUS_REG: u32 = 4;
const SP_DMA_FULL_REG: u32 = 5;
const SP_DMA_BUSY_REG: u32 = 6;
const SP_SEMAPHORE_REG: u32 = 7;
pub const SP_REGS_COUNT: u32 = 8;
const SP_MEM_ADDR_REG: usize = 0;
const SP_DRAM_ADDR_REG: usize = 1;
const SP_RD_LEN_REG: usize = 2;
const SP_WR_LEN_REG: usize = 3;
pub const SP_STATUS_REG: usize = 4;
const SP_DMA_FULL_REG: usize = 5;
const SP_DMA_BUSY_REG: usize = 6;
const SP_SEMAPHORE_REG: usize = 7;
pub const SP_REGS_COUNT: usize = 8;
pub const SP_PC_REG: u32 = 0;
//const SP_IBIST_REG: u32 = 1;
pub const SP_REGS2_COUNT: u32 = 2;
pub const SP_PC_REG: usize = 0;
//const SP_IBIST_REG: usize = 1;
pub const SP_REGS2_COUNT: usize = 2;
/* SP_STATUS - read */
pub const SP_STATUS_HALT: u32 = 1 << 0;
@@ -76,11 +76,11 @@ pub struct RspDma {
pub dramaddr: u32,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Rsp {
pub cpu: device::rsp_cpu::Cpu,
pub regs: [u32; SP_REGS_COUNT as usize],
pub regs2: [u32; SP_REGS2_COUNT as usize],
pub regs: [u32; SP_REGS_COUNT],
pub regs2: [u32; SP_REGS2_COUNT],
#[serde(with = "serde_big_array::BigArray")]
pub mem: [u8; 0x2000],
pub fifo: [RspDma; 2],
@@ -204,10 +204,10 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
j += 1;
}
}
device.rsp.regs[SP_MEM_ADDR_REG as usize] = (mem_addr & 0xfff) + (dma.memaddr & 0x1000);
device.rsp.regs[SP_DRAM_ADDR_REG as usize] = dram_addr;
device.rsp.regs[SP_RD_LEN_REG as usize] = 0xff8;
device.rsp.regs[SP_WR_LEN_REG as usize] = 0xff8;
device.rsp.regs[SP_MEM_ADDR_REG] = (mem_addr & 0xfff) + (dma.memaddr & 0x1000);
device.rsp.regs[SP_DRAM_ADDR_REG] = dram_addr;
device.rsp.regs[SP_RD_LEN_REG] = 0xff8;
device.rsp.regs[SP_WR_LEN_REG] = 0xff8;
device::events::create_event(
device,
@@ -217,52 +217,52 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
}
fn fifo_push(device: &mut device::Device, dir: DmaDir) {
if device.rsp.regs[SP_DMA_FULL_REG as usize] != 0 {
if device.rsp.regs[SP_DMA_FULL_REG] != 0 {
panic!("RSP DMA already full")
}
device.rsp.cpu.sync_point = true;
if device.rsp.regs[SP_DMA_BUSY_REG as usize] != 0 {
if device.rsp.regs[SP_DMA_BUSY_REG] != 0 {
device.rsp.fifo[1].dir = dir;
if dir == DmaDir::Read {
device.rsp.fifo[1].length = device.rsp.regs[SP_WR_LEN_REG as usize]
device.rsp.fifo[1].length = device.rsp.regs[SP_WR_LEN_REG]
} else {
device.rsp.fifo[1].length = device.rsp.regs[SP_RD_LEN_REG as usize]
device.rsp.fifo[1].length = device.rsp.regs[SP_RD_LEN_REG]
}
device.rsp.fifo[1].memaddr = device.rsp.regs[SP_MEM_ADDR_REG as usize];
device.rsp.fifo[1].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG as usize];
device.rsp.regs[SP_DMA_FULL_REG as usize] = 1;
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_DMA_FULL
device.rsp.fifo[1].memaddr = device.rsp.regs[SP_MEM_ADDR_REG];
device.rsp.fifo[1].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG];
device.rsp.regs[SP_DMA_FULL_REG] = 1;
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_DMA_FULL
} else {
device.rsp.fifo[0].dir = dir;
if dir == DmaDir::Read {
device.rsp.fifo[0].length = device.rsp.regs[SP_WR_LEN_REG as usize]
device.rsp.fifo[0].length = device.rsp.regs[SP_WR_LEN_REG]
} else {
device.rsp.fifo[0].length = device.rsp.regs[SP_RD_LEN_REG as usize]
device.rsp.fifo[0].length = device.rsp.regs[SP_RD_LEN_REG]
}
device.rsp.fifo[0].memaddr = device.rsp.regs[SP_MEM_ADDR_REG as usize];
device.rsp.fifo[0].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG as usize];
device.rsp.regs[SP_DMA_BUSY_REG as usize] = 1;
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_DMA_BUSY;
device.rsp.fifo[0].memaddr = device.rsp.regs[SP_MEM_ADDR_REG];
device.rsp.fifo[0].dramaddr = device.rsp.regs[SP_DRAM_ADDR_REG];
device.rsp.regs[SP_DMA_BUSY_REG] = 1;
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_DMA_BUSY;
do_dma(device, device.rsp.fifo[0])
}
}
pub fn fifo_pop(device: &mut device::Device) {
if device.rsp.regs[SP_DMA_FULL_REG as usize] != 0 {
if device.rsp.regs[SP_DMA_FULL_REG] != 0 {
device.rsp.fifo[0].dir = device.rsp.fifo[1].dir;
device.rsp.fifo[0].length = device.rsp.fifo[1].length;
device.rsp.fifo[0].memaddr = device.rsp.fifo[1].memaddr;
device.rsp.fifo[0].dramaddr = device.rsp.fifo[1].dramaddr;
device.rsp.regs[SP_DMA_FULL_REG as usize] = 0;
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_DMA_FULL;
device.rsp.regs[SP_DMA_FULL_REG] = 0;
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_DMA_FULL;
do_dma(device, device.rsp.fifo[0])
} else {
device.rsp.regs[SP_DMA_BUSY_REG as usize] = 0;
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_DMA_BUSY;
device.rsp.regs[SP_DMA_BUSY_REG] = 0;
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_DMA_BUSY;
if device.rsp.run_after_dma {
device.rsp.run_after_dma = false;
do_task(device);
@@ -279,7 +279,7 @@ pub fn read_regs(
device::cop0::add_cycles(device, 20);
}
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
SP_STATUS_REG => {
let value = device.rsp.regs[reg as usize]
& (SP_STATUS_SIG0
@@ -311,7 +311,7 @@ pub fn read_regs(
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
SP_STATUS_REG => update_sp_status(device, value),
SP_RD_LEN_REG => {
device::memory::masked_write_32(&mut device.rsp.regs[reg as usize], value, mask);
@@ -339,7 +339,7 @@ pub fn read_regs2(
pub fn write_regs2(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
SP_PC_REG => {
device::memory::masked_write_32(
&mut device.rsp.regs2[reg as usize],
@@ -352,20 +352,20 @@ pub fn write_regs2(device: &mut device::Device, address: u64, value: u32, mask:
}
fn update_sp_status(device: &mut device::Device, w: u32) {
let was_halted = device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_HALT != 0;
let was_halted = device.rsp.regs[SP_STATUS_REG] & SP_STATUS_HALT != 0;
/* clear / set halt */
if w & SP_CLR_HALT != 0 && w & SP_SET_HALT == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_HALT
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_HALT
}
if w & SP_SET_HALT != 0 && w & SP_CLR_HALT == 0 {
device::events::remove_event(device, device::events::EVENT_TYPE_SP);
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT
}
/* clear broke */
if w & SP_CLR_BROKE != 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_BROKE
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_BROKE
}
/* clear SP interrupt */
@@ -379,85 +379,85 @@ fn update_sp_status(device: &mut device::Device, w: u32) {
/* clear / set single step */
if w & SP_CLR_SSTEP != 0 && w & SP_SET_SSTEP == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SSTEP
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SSTEP
}
if w & SP_SET_SSTEP != 0 && w & SP_CLR_SSTEP == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SSTEP
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SSTEP
}
/* clear / set interrupt on break */
if w & SP_CLR_INTR_BREAK != 0 && w & SP_SET_INTR_BREAK == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_INTR_BREAK
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_INTR_BREAK
}
if w & SP_SET_INTR_BREAK != 0 && w & SP_CLR_INTR_BREAK == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_INTR_BREAK
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_INTR_BREAK
}
/* clear / set signal 0 */
if w & SP_CLR_SIG0 != 0 && w & SP_SET_SIG0 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG0
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG0
}
if w & SP_SET_SIG0 != 0 && w & SP_CLR_SIG0 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG0
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG0
}
/* clear / set signal 1 */
if w & SP_CLR_SIG1 != 0 && w & SP_SET_SIG1 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG1
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG1
}
if w & SP_SET_SIG1 != 0 && w & SP_CLR_SIG1 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG1
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG1
}
/* clear / set signal 2 */
if w & SP_CLR_SIG2 != 0 && w & SP_SET_SIG2 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG2
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG2
}
if w & SP_SET_SIG2 != 0 && w & SP_CLR_SIG2 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG2
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG2
}
/* clear / set signal 3 */
if w & SP_CLR_SIG3 != 0 && w & SP_SET_SIG3 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG3
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG3
}
if w & SP_SET_SIG3 != 0 && w & SP_CLR_SIG3 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG3
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG3
}
/* clear / set signal 4 */
if w & SP_CLR_SIG4 != 0 && w & SP_SET_SIG4 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG4
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG4
}
if w & SP_SET_SIG4 != 0 && w & SP_CLR_SIG4 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG4
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG4
}
/* clear / set signal 5 */
if w & SP_CLR_SIG5 != 0 && w & SP_SET_SIG5 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG5
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG5
}
if w & SP_SET_SIG5 != 0 && w & SP_CLR_SIG5 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG5
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG5
}
/* clear / set signal 6 */
if w & SP_CLR_SIG6 != 0 && w & SP_SET_SIG6 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG6
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG6
}
if w & SP_SET_SIG6 != 0 && w & SP_CLR_SIG6 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG6
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG6
}
/* clear / set signal 7 */
if w & SP_CLR_SIG7 != 0 && w & SP_SET_SIG7 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] &= !SP_STATUS_SIG7
device.rsp.regs[SP_STATUS_REG] &= !SP_STATUS_SIG7
}
if w & SP_SET_SIG7 != 0 && w & SP_CLR_SIG7 == 0 {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_SIG7
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_SIG7
}
if device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_HALT == 0 && was_halted {
if device.rsp.regs[SP_STATUS_REG] & SP_STATUS_HALT == 0 && was_halted {
device.rsp.cpu.broken = false;
device.rsp.cpu.halted = false;
do_task(device);
@@ -468,7 +468,7 @@ fn do_task(device: &mut device::Device) {
device.rsp.cpu.sync_point = false;
device.rsp.last_status_value = 0;
device.rdp.last_status_value = 0;
if device.rsp.regs[SP_DMA_BUSY_REG as usize] == 1 {
if device.rsp.regs[SP_DMA_BUSY_REG] == 1 {
device.rsp.run_after_dma = true
} else {
let timer = device::rsp_cpu::run(device);
@@ -479,21 +479,21 @@ fn do_task(device: &mut device::Device) {
pub fn rsp_event(device: &mut device::Device) {
if device.rsp.cpu.broken {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT | SP_STATUS_BROKE;
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT | SP_STATUS_BROKE;
if device.rsp.regs[SP_STATUS_REG as usize] & SP_STATUS_INTR_BREAK != 0 {
if device.rsp.regs[SP_STATUS_REG] & SP_STATUS_INTR_BREAK != 0 {
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_SP)
}
return;
}
if device.rsp.cpu.halted {
device.rsp.regs[SP_STATUS_REG as usize] |= SP_STATUS_HALT;
device.rsp.regs[SP_STATUS_REG] |= SP_STATUS_HALT;
return;
}
do_task(device)
}
pub fn init(device: &mut device::Device) {
device.rsp.regs[SP_STATUS_REG as usize] = 1;
device.rsp.regs[SP_STATUS_REG] = 1;
device::rsp_cpu::init(device);
}
+24 -35
View File
@@ -183,23 +183,22 @@ pub fn j(device: &mut device::Device, opcode: u32) {
return;
}
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc =
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 4) & 0xF0000000
| ((opcode & 0x3FFFFFF) << 2)
device.rsp.cpu.branch_state.pc = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 4)
& 0xF0000000
| ((opcode & 0x3FFFFFF) << 2)
}
pub fn jal(device: &mut device::Device, opcode: u32) {
if device::rsp_cpu::in_delay_slot_taken(device) {
device.rsp.cpu.gpr[31] = (device.rsp.cpu.branch_state.pc + 4) & 0xFFF
} else {
device.rsp.cpu.gpr[31] =
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
}
if !device::rsp_cpu::in_delay_slot_taken(device) {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc =
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 4) & 0xF0000000
| ((opcode & 0x3FFFFFF) << 2)
device.rsp.cpu.branch_state.pc = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 4)
& 0xF0000000
| ((opcode & 0x3FFFFFF) << 2)
} else if !device::rsp_cpu::in_delay_slot(device) {
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
}
@@ -208,8 +207,7 @@ pub fn jal(device: &mut device::Device, opcode: u32) {
pub fn beq(device: &mut device::Device, opcode: u32) {
if device.rsp.cpu.gpr[rs(opcode) as usize] == device.rsp.cpu.gpr[rt(opcode) as usize] {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
@@ -220,8 +218,7 @@ pub fn beq(device: &mut device::Device, opcode: u32) {
pub fn bne(device: &mut device::Device, opcode: u32) {
if device.rsp.cpu.gpr[rs(opcode) as usize] != device.rsp.cpu.gpr[rt(opcode) as usize] {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
@@ -232,8 +229,7 @@ pub fn bne(device: &mut device::Device, opcode: u32) {
pub fn blez(device: &mut device::Device, opcode: u32) {
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 <= 0 {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
@@ -244,8 +240,7 @@ pub fn blez(device: &mut device::Device, opcode: u32) {
pub fn bgtz(device: &mut device::Device, opcode: u32) {
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 > 0 {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
@@ -485,7 +480,7 @@ pub fn jalr(device: &mut device::Device, opcode: u32) {
device.rsp.cpu.gpr[rd(opcode) as usize] = (device.rsp.cpu.branch_state.pc + 4) & 0xFFF
} else {
device.rsp.cpu.gpr[rd(opcode) as usize] =
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
(device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
}
}
@@ -547,8 +542,7 @@ pub fn sltu(device: &mut device::Device, opcode: u32) {
pub fn bltz(device: &mut device::Device, opcode: u32) {
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) < 0 {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
@@ -559,8 +553,7 @@ pub fn bltz(device: &mut device::Device, opcode: u32) {
pub fn bgez(device: &mut device::Device, opcode: u32) {
if device.rsp.cpu.gpr[rs(opcode) as usize] as i32 >= 0 {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
@@ -571,34 +564,30 @@ pub fn bgez(device: &mut device::Device, opcode: u32) {
pub fn bltzal(device: &mut device::Device, opcode: u32) {
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) < 0 {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
}
device.rsp.cpu.gpr[31] =
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
}
pub fn bgezal(device: &mut device::Device, opcode: u32) {
if (device.rsp.cpu.gpr[rs(opcode) as usize] as i32) >= 0 {
device.rsp.cpu.branch_state.state = device::cpu::State::Take;
device.rsp.cpu.branch_state.pc = device.rsp.regs2
[device::rsp_interface::SP_PC_REG as usize]
device.rsp.cpu.branch_state.pc = device.rsp.regs2[device::rsp_interface::SP_PC_REG]
.wrapping_add(se16(imm(opcode) as i16) << 2)
+ 4;
} else {
device.rsp.cpu.branch_state.state = device::cpu::State::NotTaken;
}
device.rsp.cpu.gpr[31] =
(device.rsp.regs2[device::rsp_interface::SP_PC_REG as usize] + 8) & 0xFFF
device.rsp.cpu.gpr[31] = (device.rsp.regs2[device::rsp_interface::SP_PC_REG] + 8) & 0xFFF
}
pub fn mfc0(device: &mut device::Device, opcode: u32) {
device.rsp.cpu.cycle_counter += 2;
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT {
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT as u32 {
device.rsp.cpu.gpr[rt(opcode) as usize] = device::rsp_interface::read_regs(
device,
(rd(opcode) << 2) as u64,
@@ -607,14 +596,14 @@ pub fn mfc0(device: &mut device::Device, opcode: u32) {
} else {
device.rsp.cpu.gpr[rt(opcode) as usize] = device::rdp::read_regs_dpc(
device,
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT) << 2) as u64,
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT as u32) << 2) as u64,
device::memory::AccessSize::Word,
)
}
}
pub fn mtc0(device: &mut device::Device, opcode: u32) {
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT {
if rd(opcode) < device::rsp_interface::SP_REGS_COUNT as u32 {
device::rsp_interface::write_regs(
device,
(rd(opcode) << 2) as u64,
@@ -624,15 +613,15 @@ pub fn mtc0(device: &mut device::Device, opcode: u32) {
} else {
device::rdp::write_regs_dpc(
device,
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT) << 2) as u64,
((rd(opcode) - device::rsp_interface::SP_REGS_COUNT as u32) << 2) as u64,
device.rsp.cpu.gpr[rt(opcode) as usize],
0xFFFFFFFF,
)
}
if rd(opcode) == device::rsp_interface::SP_STATUS_REG
if rd(opcode) as usize == device::rsp_interface::SP_STATUS_REG
&& device.rsp.cpu.gpr[rt(opcode) as usize] & device::rsp_interface::SP_SET_HALT != 0
{
device.rsp.regs[device::rsp_interface::SP_STATUS_REG as usize] &=
device.rsp.regs[device::rsp_interface::SP_STATUS_REG] &=
!device::rsp_interface::SP_STATUS_HALT; // set halt when event happens
device.rsp.cpu.halted = true // the RSP can halt itself by setting SP_SET_HALT
}
+17 -17
View File
@@ -2,30 +2,30 @@ use crate::device;
use crate::ui;
use rand::Rng;
const SI_DRAM_ADDR_REG: u32 = 0;
const SI_PIF_ADDR_RD64B_REG: u32 = 1;
//const SI_R2_REG: u32 = 2;
//const SI_R3_REG: u32 = 3;
const SI_PIF_ADDR_WR64B_REG: u32 = 4;
//const SI_R5_REG: u32 = 5;
pub const SI_STATUS_REG: u32 = 6;
pub const SI_REGS_COUNT: u32 = 7;
const SI_DRAM_ADDR_REG: usize = 0;
const SI_PIF_ADDR_RD64B_REG: usize = 1;
//const SI_R2_REG: usize = 2;
//const SI_R3_REG: usize = 3;
const SI_PIF_ADDR_WR64B_REG: usize = 4;
//const SI_R5_REG: usize = 5;
pub const SI_STATUS_REG: usize = 6;
pub const SI_REGS_COUNT: usize = 7;
pub const SI_STATUS_DMA_BUSY: u32 = 1 << 0;
pub const SI_STATUS_IO_BUSY: u32 = 1 << 1;
//const SI_STATUS_DMA_ERROR: u32 = 1 << 3;
const SI_STATUS_INTERRUPT: u32 = 1 << 12;
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DmaDir {
None,
Write,
Read,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Si {
pub regs: [u32; SI_REGS_COUNT as usize],
pub regs: [u32; SI_REGS_COUNT],
pub dma_dir: DmaDir,
}
@@ -47,7 +47,7 @@ fn dma_read(device: &mut device::Device) {
let duration = device::pif::update_pif_ram(device);
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_DMA_BUSY;
device.si.regs[SI_STATUS_REG] |= SI_STATUS_DMA_BUSY;
let length = duration + randomize_interrupt_time(&mut device.rng);
@@ -59,7 +59,7 @@ fn dma_write(device: &mut device::Device) {
copy_pif_rdram(device);
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_DMA_BUSY;
device.si.regs[SI_STATUS_REG] |= SI_STATUS_DMA_BUSY;
let length = 6000 + randomize_interrupt_time(&mut device.rng); //based on https://github.com/rasky/n64-systembench
@@ -68,7 +68,7 @@ fn dma_write(device: &mut device::Device) {
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
SI_STATUS_REG => {
device.si.regs[reg as usize] &= !SI_STATUS_INTERRUPT;
device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_SI)
@@ -81,7 +81,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
//rdram is in native endian format, and pif memory is in big endian format
fn copy_pif_rdram(device: &mut device::Device) {
let dram_addr = device.si.regs[SI_DRAM_ADDR_REG as usize] as usize & device::rdram::RDRAM_MASK;
let dram_addr = device.si.regs[SI_DRAM_ADDR_REG] as usize & device::rdram::RDRAM_MASK;
if device.si.dma_dir == DmaDir::Write {
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
@@ -125,8 +125,8 @@ pub fn dma_event(device: &mut device::Device) {
panic!("si dma unknown")
}
device.si.dma_dir = DmaDir::None;
device.si.regs[SI_STATUS_REG as usize] &= !(SI_STATUS_DMA_BUSY | SI_STATUS_IO_BUSY);
device.si.regs[SI_STATUS_REG as usize] |= SI_STATUS_INTERRUPT;
device.si.regs[SI_STATUS_REG] &= !(SI_STATUS_DMA_BUSY | SI_STATUS_IO_BUSY);
device.si.regs[SI_STATUS_REG] |= SI_STATUS_INTERRUPT;
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_SI)
}
+61 -53
View File
@@ -34,22 +34,22 @@ pub fn read(device: &mut device::Device, index: u64) {
if index > 31 {
return;
}
device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG as usize] =
device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG] =
device.cpu.cop0.tlb_entries[index as usize].mask << 13;
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] =
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] =
((device.cpu.cop0.tlb_entries[index as usize].region as u64) << 62)
| (device.cpu.cop0.tlb_entries[index as usize].vpn2 << 13)
| (device.cpu.cop0.tlb_entries[index as usize].asid) as u64;
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] =
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] =
(device.cpu.cop0.tlb_entries[index as usize].pfn_even << 6)
| (device.cpu.cop0.tlb_entries[index as usize].c_even << 3) as u64
| (device.cpu.cop0.tlb_entries[index as usize].d_even << 2) as u64
| (device.cpu.cop0.tlb_entries[index as usize].v_even << 1) as u64
| (device.cpu.cop0.tlb_entries[index as usize].g) as u64;
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] =
device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] =
(device.cpu.cop0.tlb_entries[index as usize].pfn_odd << 6)
| (device.cpu.cop0.tlb_entries[index as usize].c_odd << 3) as u64
| (device.cpu.cop0.tlb_entries[index as usize].d_odd << 2) as u64
@@ -64,37 +64,37 @@ pub fn write(device: &mut device::Device, index: u64) {
tlb_unmap(device, index);
device.cpu.cop0.tlb_entries[index as usize].g = (device.cpu.cop0.regs
[device::cop0::COP0_ENTRYLO0_REG as usize]
& device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize]
[device::cop0::COP0_ENTRYLO0_REG]
& device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG]
& 1) as u8;
device.cpu.cop0.tlb_entries[index as usize].pfn_even =
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 6) & 0xFFFFF;
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 6) & 0xFFFFF;
device.cpu.cop0.tlb_entries[index as usize].pfn_odd =
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 6) & 0xFFFFF;
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 6) & 0xFFFFF;
device.cpu.cop0.tlb_entries[index as usize].c_even =
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 3) & 7) as u8;
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 3) & 7) as u8;
device.cpu.cop0.tlb_entries[index as usize].c_odd =
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 3) & 7) as u8;
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 3) & 7) as u8;
device.cpu.cop0.tlb_entries[index as usize].d_even =
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 2) & 1) as u8;
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 2) & 1) as u8;
device.cpu.cop0.tlb_entries[index as usize].d_odd =
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 2) & 1) as u8;
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 2) & 1) as u8;
device.cpu.cop0.tlb_entries[index as usize].v_even =
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG as usize] >> 1) & 1) as u8;
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO0_REG] >> 1) & 1) as u8;
device.cpu.cop0.tlb_entries[index as usize].v_odd =
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG as usize] >> 1) & 1) as u8;
((device.cpu.cop0.regs[device::cop0::COP0_ENTRYLO1_REG] >> 1) & 1) as u8;
device.cpu.cop0.tlb_entries[index as usize].asid =
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] as u8;
device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] as u8;
device.cpu.cop0.tlb_entries[index as usize].mask =
(device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG as usize] >> 13) & 0xFFF;
(device.cpu.cop0.regs[device::cop0::COP0_PAGEMASK_REG] >> 13) & 0xFFF;
device.cpu.cop0.tlb_entries[index as usize].vpn2 =
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 13) & 0x7FFFFFF;
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 13) & 0x7FFFFFF;
device.cpu.cop0.tlb_entries[index as usize].region =
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 62) as u8;
(device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 62) as u8;
device.cpu.cop0.tlb_entries[index as usize].mask &= 0b101010101010;
device.cpu.cop0.tlb_entries[index as usize].mask |=
@@ -125,22 +125,20 @@ pub fn write(device: &mut device::Device, index: u64) {
}
pub fn probe(device: &mut device::Device) {
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG as usize] = 0x80000000; // set probe failure
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG] = 0x80000000; // set probe failure
for (pos, e) in device.cpu.cop0.tlb_entries.iter().enumerate() {
if e.vpn2 & !e.mask
!= ((device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 13) & 0x7FFFFFF)
& !e.mask
!= ((device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 13) & 0x7FFFFFF) & !e.mask
{
continue;
}
if e.region != (device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] >> 62) as u8 {
if e.region != (device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] >> 62) as u8 {
continue;
}
if e.g == 0 && e.asid != device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG as usize] as u8
{
if e.g == 0 && e.asid != device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG] as u8 {
continue;
}
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG as usize] = pos as u64;
device.cpu.cop0.regs[device::cop0::COP0_INDEX_REG] = pos as u64;
break;
}
}
@@ -151,15 +149,13 @@ fn tlb_unmap(device: &mut device::Device, index: u64) {
if e.v_even != 0 {
let mut i = e.start_even;
while i < e.end_even {
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address = 0;
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = false;
device.cpu.cop0.tlb_lut_r.remove(&(i >> 12));
i += 0x1000
}
if e.d_even != 0 {
let mut i = e.start_even;
while i < e.end_even {
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address = 0;
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = false;
device.cpu.cop0.tlb_lut_w.remove(&(i >> 12));
i += 0x1000
}
}
@@ -168,15 +164,13 @@ fn tlb_unmap(device: &mut device::Device, index: u64) {
if e.v_odd != 0 {
let mut i = e.start_odd;
while i < e.end_odd {
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address = 0;
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = false;
device.cpu.cop0.tlb_lut_r.remove(&(i >> 12));
i += 0x1000
}
if e.d_odd != 0 {
let mut i = e.start_odd;
while i < e.end_odd {
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address = 0;
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = false;
device.cpu.cop0.tlb_lut_w.remove(&(i >> 12));
i += 0x1000
}
}
@@ -193,17 +187,25 @@ fn tlb_map(device: &mut device::Device, index: u64) {
{
let mut i = e.start_even;
while i < e.end_even {
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address =
0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF);
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = e.c_even != 2;
device.cpu.cop0.tlb_lut_r.insert(
i >> 12,
device::tlb::TlbLut {
address: 0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF),
cached: e.c_even != 2,
},
);
i += 0x1000
}
if e.d_even != 0 {
let mut i = e.start_even;
while i < e.end_even {
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address =
0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF);
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = e.c_even != 2;
device.cpu.cop0.tlb_lut_w.insert(
i >> 12,
device::tlb::TlbLut {
address: 0x80000000 | (e.phys_even + (i - e.start_even) + 0xFFF),
cached: e.c_even != 2,
},
);
i += 0x1000
}
}
@@ -216,17 +218,25 @@ fn tlb_map(device: &mut device::Device, index: u64) {
{
let mut i = e.start_odd;
while i < e.end_odd {
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].address =
0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF);
device.cpu.cop0.tlb_lut_r[(i >> 12) as usize].cached = e.c_odd != 2;
device.cpu.cop0.tlb_lut_r.insert(
i >> 12,
device::tlb::TlbLut {
address: 0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF),
cached: e.c_odd != 2,
},
);
i += 0x1000
}
if e.d_odd != 0 {
let mut i = e.start_odd;
while i < e.end_odd {
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].address =
0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF);
device.cpu.cop0.tlb_lut_w[(i >> 12) as usize].cached = e.c_odd != 2;
device.cpu.cop0.tlb_lut_w.insert(
i >> 12,
device::tlb::TlbLut {
address: 0x80000000 | (e.phys_odd + (i - e.start_odd) + 0xFFF),
cached: e.c_odd != 2,
},
);
i += 0x1000
}
}
@@ -241,19 +251,17 @@ pub fn get_physical_address(
address &= 0xffffffff;
if access_type == device::memory::AccessType::Write {
if device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].address != 0 {
if let Some(lut) = device.cpu.cop0.tlb_lut_w.get(&(address >> 12)) {
return (
(device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].address & 0x1FFFF000)
| (address & 0xFFF),
device.cpu.cop0.tlb_lut_w[(address >> 12) as usize].cached,
(lut.address & 0x1FFFF000) | (address & 0xFFF),
lut.cached,
false,
);
}
} else if device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].address != 0 {
} else if let Some(lut) = device.cpu.cop0.tlb_lut_r.get(&(address >> 12)) {
return (
(device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].address & 0x1FFFF000)
| (address & 0xFFF),
device.cpu.cop0.tlb_lut_r[(address >> 12) as usize].cached,
(lut.address & 0x1FFFF000) | (address & 0xFFF),
lut.cached,
false,
);
}
+124 -95
View File
@@ -1,57 +1,46 @@
use crate::{cheats, device, netplay, retroachievements, ui};
use crate::{cheats, device, netplay, retroachievements, savestates, ui};
const VI_STATUS_REG: u32 = 0;
const VI_ORIGIN_REG: u32 = 1;
//const VI_WIDTH_REG: u32 = 2;
//const VI_V_INTR_REG: u32 = 3;
const VI_CURRENT_REG: u32 = 4;
//const VI_BURST_REG: u32 = 5;
const VI_V_SYNC_REG: u32 = 6;
const VI_H_SYNC_REG: u32 = 7;
//const VI_LEAP_REG: u32 = 8;
//const VI_H_START_REG: u32 = 9;
//const VI_V_START_REG: u32 = 10;
//const VI_V_BURST_REG: u32 = 11;
//const VI_X_SCALE_REG: u32 = 12;
//const VI_Y_SCALE_REG: u32 = 13;
pub const VI_REGS_COUNT: u32 = 14;
const VI_STATUS_REG: usize = 0;
const VI_ORIGIN_REG: usize = 1;
//const VI_WIDTH_REG: usize = 2;
const VI_V_INTR_REG: usize = 3;
const VI_CURRENT_REG: usize = 4;
//const VI_BURST_REG: usize = 5;
const VI_V_SYNC_REG: usize = 6;
const VI_H_SYNC_REG: usize = 7;
//const VI_LEAP_REG: usize = 8;
//const VI_H_START_REG: usize = 9;
const VI_V_START_REG: usize = 10;
//const VI_V_BURST_REG: usize = 11;
//const VI_X_SCALE_REG: usize = 12;
//const VI_Y_SCALE_REG: usize = 13;
pub const VI_REGS_COUNT: usize = 14;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Vi {
pub regs: [u32; VI_REGS_COUNT as usize],
pub regs: [u32; VI_REGS_COUNT],
pub clock: u64,
pub delay: u64,
pub field: u32,
#[serde(skip)]
pub next_pace_deadline: Option<std::time::Instant>,
pub count_per_scanline: u64,
pub enable_speed_limiter: bool,
pub vi_counter: u64,
pub min_wait_time: std::time::Duration,
pub frame_time: f64,
pub limit_freq: u64,
#[serde(skip)]
#[serde(default = "std::time::Instant::now")]
pub limit_freq_check: std::time::Instant,
pub elapsed_time: f64,
}
const MAX_LIMIT_FREQ: u64 = 3;
pub fn set_expected_refresh_rate(device: &mut device::Device) {
let expected_refresh_rate = device.vi.clock as f64
/ (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as f64
/ ((device.vi.regs[VI_H_SYNC_REG as usize] & 0xFFF) + 1) as f64
/ (device.vi.regs[VI_V_SYNC_REG] + 1) as f64
/ ((device.vi.regs[VI_H_SYNC_REG] & 0xFFF) + 1) as f64
* 2.0;
device.vi.frame_time = 1.0 / expected_refresh_rate;
device.vi.delay = (device.cpu.clock_rate as f64 / expected_refresh_rate) as u64;
device.vi.count_per_scanline =
device.vi.delay / (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as u64;
reset_pace_deadline(device);
device.vi.count_per_scanline = device.vi.delay / (device.vi.regs[VI_V_SYNC_REG] + 1) as u64;
}
fn reset_pace_deadline(device: &mut device::Device) {
device.vi.next_pace_deadline = None;
device.speed_limiter.next_pace_deadline = None;
}
fn set_vertical_interrupt(device: &mut device::Device) {
@@ -62,19 +51,19 @@ fn set_vertical_interrupt(device: &mut device::Device) {
fn set_current_line(device: &mut device::Device) {
if let Some(next_vi) = device::events::get_event(device, device::events::EVENT_TYPE_VI) {
device.vi.regs[VI_CURRENT_REG as usize] = ((device.vi.delay.saturating_sub(
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize],
)) / device.vi.count_per_scanline) as u32;
device.vi.regs[VI_CURRENT_REG] =
((device.vi.delay.saturating_sub(
next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG],
)) / device.vi.count_per_scanline) as u32;
// wrap around VI_CURRENT_REG if needed
if device.vi.regs[VI_CURRENT_REG as usize] >= device.vi.regs[VI_V_SYNC_REG as usize] {
device.vi.regs[VI_CURRENT_REG as usize] -= device.vi.regs[VI_V_SYNC_REG as usize]
if device.vi.regs[VI_CURRENT_REG] >= device.vi.regs[VI_V_SYNC_REG] {
device.vi.regs[VI_CURRENT_REG] -= device.vi.regs[VI_V_SYNC_REG]
}
}
/* update current field */
device.vi.regs[VI_CURRENT_REG as usize] =
(device.vi.regs[VI_CURRENT_REG as usize] & !1) | device.vi.field;
ui::video::set_register(VI_CURRENT_REG, device.vi.regs[VI_CURRENT_REG as usize])
device.vi.regs[VI_CURRENT_REG] = (device.vi.regs[VI_CURRENT_REG] & !1) | device.vi.field;
ui::video::set_register(VI_CURRENT_REG as u32, device.vi.regs[VI_CURRENT_REG])
}
pub fn read_regs(
@@ -83,7 +72,7 @@ pub fn read_regs(
_access_size: device::memory::AccessSize,
) -> u32 {
let reg = (address & 0xFFFF) >> 2;
if reg as u32 == VI_CURRENT_REG {
if reg as usize == VI_CURRENT_REG {
set_current_line(device)
}
device::cop0::add_cycles(device, 20);
@@ -92,7 +81,7 @@ pub fn read_regs(
pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u32) {
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
match reg as usize {
VI_CURRENT_REG => device::mi::clear_rcp_interrupt(device, device::mi::MI_INTR_VI),
VI_V_SYNC_REG => {
if device.vi.regs[reg as usize] != value & mask {
@@ -111,7 +100,12 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
let current_origin = device.vi.regs[reg as usize];
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
if current_origin != device.vi.regs[reg as usize] {
let _ = device.ui.video.fps_tx.try_send(true);
if device.netplay.is_none() {
savestates::process_savestates(device);
}
if !netplay::in_rollback(device.netplay.as_ref()) {
let _ = device.ui.video.fps_tx.as_ref().unwrap().send(true);
}
}
}
_ => {
@@ -121,45 +115,58 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
ui::video::set_register(reg as u32, device.vi.regs[reg as usize])
}
pub fn update_screen(device: &mut device::Device) {
if !netplay::in_rollback(device.netplay.as_ref()) {
ui::video::render_frame();
}
let (speed_limiter_toggled, paused) = ui::video::check_callback(device);
if speed_limiter_toggled {
reset_pace_deadline(device);
}
if !netplay::in_rollback(device.netplay.as_ref())
&& device
.speed_limiter
.frame_counter
.is_multiple_of(device.speed_limiter.limit_freq)
&& device.speed_limiter.enabled
{
speed_limiter(device);
}
if !netplay::in_rollback(device.netplay.as_ref()) {
ui::video::update_screen();
device.speed_limiter.frame_counter += 1;
let _ = device.ui.video.vis_tx.as_ref().unwrap().send(true);
}
if device.netplay.is_some() {
device.netplay.as_mut().unwrap().inputs = netplay::process_requests(device);
} else if paused {
if retroachievements::get_hardcore() {
ui::video::onscreen_message(
"Cannot pause in RA hardcore mode",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else {
ui::video::pause_loop(&mut device.ui, device.vi.frame_time);
}
}
}
pub fn vertical_interrupt_event(device: &mut device::Device) {
device.vi.elapsed_time += device.vi.frame_time;
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
ui::video::render_frame();
let _ = device.ui.video.vis_tx.try_send(true);
retroachievements::do_frame();
let (mut speed_limiter_toggled, paused) = ui::video::check_callback(device);
if let Some(netplay) = &mut device.netplay {
netplay::send_sync_check(netplay, device.cpu.cop0.regs.as_ref());
if device.vi.enable_speed_limiter == netplay.fast_forward {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = !netplay.fast_forward;
}
}
if device.vi.vi_counter.is_multiple_of(device.vi.limit_freq) && device.vi.enable_speed_limiter {
speed_limiter(device, speed_limiter_toggled);
}
ui::video::update_screen();
device.vi.vi_counter += 1;
if device.netplay.is_none() && paused {
if retroachievements::get_hardcore() {
ui::video::onscreen_message("Cannot pause in RA hardcore mode", false);
} else {
ui::video::pause_loop(device.vi.frame_time);
}
}
unsafe { sdl3_sys::events::SDL_PumpEvents() };
/* toggle vi field if in interlaced mode */
device.vi.field ^= (device.vi.regs[VI_STATUS_REG as usize] >> 6) & 0x1;
device.vi.field ^= (device.vi.regs[VI_STATUS_REG] >> 6) & 0x1;
device::mi::set_rcp_interrupt(device, device::mi::MI_INTR_VI);
@@ -167,7 +174,17 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
device,
device::events::EVENT_TYPE_VI,
device.cpu.next_event_count + device.vi.delay,
)
);
let v_intr = device.vi.regs[VI_V_INTR_REG] & 0x3FF;
let v_start = (device.vi.regs[VI_V_START_REG] >> 16) & 0x3FF;
if v_start > v_intr {
device::events::create_event(
device,
device::events::EVENT_TYPE_UPDATE_SCREEN,
(v_start - v_intr) as u64 * device.vi.count_per_scanline,
);
}
}
pub fn init(device: &mut device::Device) {
@@ -178,56 +195,68 @@ pub fn init(device: &mut device::Device) {
}
}
fn speed_limiter(device: &mut device::Device, mut speed_limiter_toggled: bool) {
let interval =
std::time::Duration::from_secs_f64(device.vi.frame_time * device.vi.limit_freq as f64);
fn speed_limiter(device: &mut device::Device) {
let mut speed_limiter_toggled = false;
let mut interval = std::time::Duration::from_secs_f64(
device.vi.frame_time * device.speed_limiter.limit_freq as f64,
);
if let Some(netplay) = &device.netplay {
let ahead = netplay.session.frames_ahead();
if ahead > 0 {
interval = interval.mul_f64(1.0 + 0.05 * ahead.min(2) as f64);
} else if ahead < 0 {
interval = interval.mul_f64(1.0 - 0.05 * (-ahead).min(2) as f64);
}
}
let now = std::time::Instant::now();
match device.vi.next_pace_deadline {
match device.speed_limiter.next_pace_deadline {
None => {
device.vi.next_pace_deadline = Some(now + interval);
device.speed_limiter.next_pace_deadline = Some(now + interval);
speed_limiter_toggled = true;
}
Some(deadline) => {
if now < deadline {
let dur = deadline - now;
spin_sleep::sleep(dur);
if dur < device.vi.min_wait_time {
device.vi.min_wait_time = dur;
if dur < device.speed_limiter.min_wait_time {
device.speed_limiter.min_wait_time = dur;
}
} else {
//println!("did not sleep");
device.vi.min_wait_time = std::time::Duration::from_secs(0);
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(0);
}
let mut next = deadline + interval;
let t = std::time::Instant::now();
while next <= t {
next += interval;
}
device.vi.next_pace_deadline = Some(next);
device.speed_limiter.next_pace_deadline = Some(next);
}
}
if std::time::Instant::now().duration_since(device.vi.limit_freq_check)
if std::time::Instant::now().duration_since(device.speed_limiter.limit_freq_check)
> std::time::Duration::from_secs(1)
{
if !speed_limiter_toggled {
if device.vi.min_wait_time == std::time::Duration::from_secs(0)
&& device.vi.limit_freq < MAX_LIMIT_FREQ
if device.speed_limiter.min_wait_time == std::time::Duration::from_secs(0)
&& device.speed_limiter.limit_freq < MAX_LIMIT_FREQ
{
device.vi.limit_freq += 1;
device.speed_limiter.limit_freq += 1;
reset_pace_deadline(device);
} else if device.vi.min_wait_time
} else if device.speed_limiter.min_wait_time
> std::time::Duration::from_secs_f64(device.vi.frame_time)
&& device.vi.limit_freq > 1
&& device.speed_limiter.limit_freq > 1
{
device.vi.limit_freq -= 1;
device.speed_limiter.limit_freq -= 1;
reset_pace_deadline(device);
}
}
//println!("limit freq: {}", device.vi.limit_freq);
device.vi.min_wait_time = std::time::Duration::from_secs(1);
device.vi.limit_freq_check = std::time::Instant::now();
device.speed_limiter.min_wait_time = std::time::Duration::from_secs(1);
device.speed_limiter.limit_freq_check = std::time::Instant::now();
}
}
+398
View File
@@ -0,0 +1,398 @@
#![deny(warnings)]
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
#[cfg(target_os = "android")]
use slint::ComponentHandle;
use std::io::Error;
#[cfg(target_os = "android")]
use ui::android;
/// N64 emulator
#[derive(Parser, Debug)]
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
pub struct Args {
pub game: Option<String>,
#[arg(short, long)]
pub fullscreen: bool,
#[arg(long)]
pub widescreen: bool,
#[arg(long)]
pub overclock: Option<bool>,
#[arg(long)]
pub disable_expansion_pak: Option<bool>,
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
pub cheats: Option<String>,
#[arg(long, value_name = "NETPLAY_SERVER_ADDR", hide = true)]
pub netplay_server_addr: Option<String>,
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
pub netplay_player_number: Option<usize>,
#[arg(long, value_name = "NETPLAY_NUMBER_OF_PLAYERS", hide = true)]
pub netplay_number_of_players: Option<usize>,
#[arg(long, value_name = "NETPLAY_INPUT_DELAY", hide = true)]
pub netplay_input_delay: Option<usize>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
pub configure_input_profile: Option<String>,
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
pub use_dinput: bool,
#[arg(
long,
value_name = "DEADZONE_PERCENTAGE",
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
)]
pub deadzone: Option<i32>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Must also specify --port. Used to bind a previously created profile to a port"
)]
pub bind_input_profile: Option<String>,
#[arg(
long,
help = "Lists connected controllers which can be used in --assign-controller"
)]
pub list_controllers: bool,
#[arg(
long,
value_name = "CONTROLLER_NUMBER",
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
)]
pub assign_controller: Option<i32>,
#[arg(
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
pub port: Option<usize>,
#[arg(
long,
help = "Clear all input profile bindings and controller assignments"
)]
pub clear_input_bindings: bool,
#[arg(
long,
value_name = "SLOT",
help = "Load savestate from slot 0-9 when starting the game"
)]
pub load_state: Option<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
pub ra_username: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
pub ra_password: Option<String>,
}
#[cfg(feature = "gui")]
fn set_app_id() {
if let Err(e) = slint::set_xdg_app_id(ui::APP_ID) {
eprintln!("Could not set application ID: {}", e);
}
}
pub fn create_runtime() -> (tokio::sync::oneshot::Sender<()>, tokio::runtime::Handle) {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let (close_tx, close_rx) = tokio::sync::oneshot::channel::<()>();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to build runtime");
tx.send(rt.handle().clone()).unwrap();
rt.block_on(close_rx).unwrap();
});
(close_tx, rx.blocking_recv().unwrap())
}
pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
let dirs = ui::get_dirs();
std::fs::create_dir_all(&dirs.config_dir)?;
std::fs::create_dir_all(&dirs.cache_dir)?;
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
ui::sdl_hints();
if let Some(game) = args.game {
let file_path = std::path::Path::new(&game).to_path_buf();
let Some(rom_contents) = device::get_rom_contents(&file_path) else {
return Err(Error::other(format!(
"Could not read ROM file: {}",
file_path.display()
)));
};
let cheats = if let Some(cheats_file) = args.cheats {
if let Ok(data) = std::fs::read(cheats_file)
&& let Ok(cheats) = serde_json::from_slice(&data)
{
cheats
} else {
return Err(Error::other("Could not read cheats file"));
}
} else {
let game_crc = ui::storage::get_game_crc(&rom_contents);
ui::config::Cheats::new()
.cheats
.get(&game_crc)
.cloned()
.unwrap_or_default()
};
if let Some(slot) = args.load_state
&& slot > 9
{
return Err(Error::other("Savestate slot must be between 0 and 9"));
}
let mut device = device::Device::new(true);
device.ui.config.ui.recent_roms.retain(|x| *x != game);
device.ui.config.ui.recent_roms.insert(0, game);
device.ui.config.ui.recent_roms.truncate(5);
if args.fullscreen {
device.ui.video.fullscreen = true;
} else {
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
}
if args.widescreen {
device.ui.video.widescreen = true;
} else {
device.ui.video.widescreen = device.ui.config.video.widescreen;
}
let overclock = args
.overclock
.unwrap_or(device.ui.config.emulation.overclock);
let disable_expansion_pak = args
.disable_expansion_pak
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
let mut shutdown_tx = None;
let mut usb_handle = None;
if args.netplay_server_addr.is_none() {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_rom_path[i].is_empty()
&& !device.ui.config.input.gb_ram_path[i].is_empty()
&& let Ok(rom) = std::fs::read(&device.ui.config.input.gb_rom_path[i])
&& let Ok(ram) = std::fs::read(&device.ui.config.input.gb_ram_path[i])
{
device::controller::gbcart::init(&mut device.transferpaks[i].cart, &rom, &ram);
}
}
if device.ui.config.emulation.usb {
(shutdown_tx, usb_handle, device.ui.usb) = ui::usb::init();
}
}
let file_path = dirs.config_dir.join("retroachievements.json");
let ra_config = if let ra_config = std::fs::read(&file_path).unwrap_or_default()
&& let ra_config =
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
.unwrap_or_default()
&& (ra_config.enabled || args.ra_username.is_some())
{
let username = args.ra_username.unwrap_or(ra_config.username.clone());
retroachievements::init_client(
if cfg!(ra_hardcore_enabled) && args.netplay_server_addr.is_none() {
ra_config.hardcore
} else {
false
},
ra_config.challenge,
ra_config.leaderboard,
);
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
if let Some(password) = args.ra_password {
retroachievements::login_user(username, password, tx);
} else if !ra_config.token.is_empty() {
retroachievements::login_token_user(username, ra_config.token.clone(), tx);
} else {
tx.send(false).unwrap();
}
rx.blocking_recv().unwrap();
ra_config
} else {
retroachievements::RAConfig::default()
};
let netplay_config = if let Some(server_addr) = args.netplay_server_addr
&& let Some(player_number) = args.netplay_player_number
&& let Some(number_of_players) = args.netplay_number_of_players
&& let Some(input_delay) = args.netplay_input_delay
{
Some(netplay::NetplayConfig {
server_addr,
player_number,
number_of_players,
input_delay,
ice_config_path: dirs.cache_dir.join("ice_config.json"),
})
} else {
None
};
device::run_game(
&mut device,
&rom_contents,
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats,
load_savestate_slot: args.load_state,
},
ra_config,
netplay_config,
);
// on Android, the client is shut down in the app_window function
#[cfg(not(target_os = "android"))]
retroachievements::shutdown_client();
if device.netplay.is_none() {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_ram_path[i].is_empty()
&& !device.transferpaks[i].cart.ram.is_empty()
{
device::controller::gbcart::save(
&mut device.transferpaks[i].cart,
device.vi.elapsed_time as i64,
&device.ui.config.input.gb_ram_path[i],
);
}
}
}
ui::usb::close(shutdown_tx, usb_handle);
} else if arg_count > 1 {
let mut config = ui::config::Config::new();
if let Some(profile) = args.configure_input_profile {
ui::input_profile::configure_input_profile(
&mut config,
profile,
args.use_dinput,
args.deadzone.unwrap_or(ui::input_profile::DEADZONE_DEFAULT),
);
ui::sdl_close();
return Ok(());
} else {
if args.clear_input_bindings {
ui::input::clear_bindings(&mut config);
return Ok(());
}
if let Some(port) = args.port
&& !(1..=4).contains(&port)
{
return Err(Error::other("Port must be between 1 and 4"));
}
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
if args.list_controllers {
let controllers = ui::input::get_controller_names();
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {i}: {controller}");
}
} else {
if let Some(assign_controller) = args.assign_controller {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::assign_controller(&mut config, assign_controller - 1, port);
}
if let Some(profile) = args.bind_input_profile {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::bind_input_profile(&mut config, profile, port);
}
}
}
} else {
#[cfg(feature = "gui")]
{
let app = ui::gui::AppWindow::new().unwrap();
let no_intro_map =
std::sync::Arc::new(tokio::sync::Mutex::new(rustc_hash::FxHashMap::default()));
set_app_id();
ui::gui::app_window(&app, false, no_intro_map);
}
}
ui::sdl_close();
Ok(())
}
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(app: slint::android::AndroidApp) {
let (close_tx, handle) = create_runtime();
let _guard = handle.enter();
slint::android::init_with_event_listener(app.clone(), move |event| match event {
slint::android::android_activity::PollEvent::Main(main_event) => match main_event {
slint::android::android_activity::MainEvent::TerminateWindow { .. } => {
if let Ok(weak_app_window) = android::WEAK_SLINT_WINDOW.lock()
&& let Some(weak_app_window) = weak_app_window.as_ref()
{
weak_app_window
.upgrade_in_event_loop(move |handle| {
ui::gui::save_settings(&handle);
handle.invoke_netplay_close();
})
.unwrap();
}
}
_ => {}
},
_ => {}
})
.unwrap();
let app_window = ui::gui::AppWindow::new().unwrap();
*android::WEAK_SLINT_WINDOW.lock().unwrap() = Some(app_window.as_weak());
*android::ANDROID_APP.lock().unwrap() = Some(app);
let dirs = ui::get_dirs();
std::fs::create_dir_all(&dirs.config_dir).unwrap();
std::fs::create_dir_all(&dirs.cache_dir).unwrap();
std::fs::create_dir_all(dirs.data_dir.join("saves")).unwrap();
std::fs::create_dir_all(dirs.data_dir.join("states")).unwrap();
let no_intro_map =
std::sync::Arc::new(tokio::sync::Mutex::new(rustc_hash::FxHashMap::default()));
ui::gui::app_window(&app_window, true, no_intro_map);
close_tx.send(()).unwrap();
*android::WEAK_SLINT_WINDOW.lock().unwrap() = None;
*android::ANDROID_APP.lock().unwrap() = None;
}
+8 -292
View File
@@ -3,298 +3,14 @@
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
use std::io::Error;
/// N64 emulator
#[derive(Parser, Debug)]
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
struct Args {
game: Option<String>,
#[arg(short, long)]
fullscreen: bool,
#[arg(long)]
overclock: Option<bool>,
#[arg(long)]
disable_expansion_pak: Option<bool>,
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
cheats: Option<String>,
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
netplay_peer_addr: Option<String>,
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
netplay_player_number: Option<u8>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
configure_input_profile: Option<String>,
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
use_dinput: bool,
#[arg(
long,
value_name = "DEADZONE_PERCENTAGE",
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
)]
deadzone: Option<i32>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Must also specify --port. Used to bind a previously created profile to a port"
)]
bind_input_profile: Option<String>,
#[arg(
long,
help = "Lists connected controllers which can be used in --assign-controller"
)]
list_controllers: bool,
#[arg(
long,
value_name = "CONTROLLER_NUMBER",
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
)]
assign_controller: Option<i32>,
#[arg(
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
port: Option<usize>,
#[arg(
long,
help = "Clear all input profile bindings and controller assignments"
)]
clear_input_bindings: bool,
#[arg(
long,
value_name = "SLOT",
help = "Load savestate from slot 0-9 when starting the game"
)]
load_state: Option<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
ra_username: Option<String>,
#[arg(
long = "ra-token",
value_name = "TOKEN",
help = "Token for RetroAchievements",
hide = true
)]
ra_token: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
ra_password: Option<String>,
#[arg(
long = "ra-hardcore",
help = "Enable Hardcore mode for RetroAchievements"
)]
ra_hardcore: bool,
#[arg(
long = "ra-challenge",
help = "Enable Challenge Indicators for RetroAchievements"
)]
ra_challenge: bool,
#[arg(
long = "ra-leaderboard",
help = "Enable Leaderboard Trackers for RetroAchievements"
)]
ra_leaderboard: bool,
}
#[tokio::main(worker_threads = 4)]
async fn main() -> std::io::Result<()> {
let dirs = ui::get_dirs();
std::fs::create_dir_all(dirs.config_dir)?;
std::fs::create_dir_all(dirs.cache_dir)?;
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
let args = Args::parse();
if let Some(game) = args.game {
let file_path = std::path::Path::new(&game);
let Some(rom_contents) = device::get_rom_contents(file_path) else {
return Err(Error::other(format!(
"Could not read ROM file: {}",
file_path.display()
)));
};
if let Some(slot) = args.load_state
&& slot > 9
{
return Err(Error::other("Savestate slot must be between 0 and 9"));
}
let mut device = device::Device::new();
device.ui.config.recent_roms.retain(|x| *x != game);
device.ui.config.recent_roms.insert(0, game);
device.ui.config.recent_roms.truncate(5);
if args.fullscreen {
device.ui.video.fullscreen = true;
} else {
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
}
let overclock = args
.overclock
.unwrap_or(device.ui.config.emulation.overclock);
let disable_expansion_pak = args
.disable_expansion_pak
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
let cheats = if let Some(cheats_file) = args.cheats
&& let Ok(data) = std::fs::read(cheats_file)
&& let Ok(cheats) = serde_json::from_slice(&data)
{
cheats
} else {
let game_crc = ui::storage::get_game_crc(&rom_contents);
ui::config::Cheats::new()
.cheats
.get(&game_crc)
.cloned()
.unwrap_or_default()
};
let mut shutdown_tx = None;
if let Some(peer_addr) = args.netplay_peer_addr
&& let Some(player_number) = args.netplay_player_number
{
device.netplay = Some(netplay::init(peer_addr.parse().unwrap(), player_number));
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_rom_path[i].is_empty()
&& !device.ui.config.input.gb_ram_path[i].is_empty()
{
device.transferpaks[i].cart.rom =
std::fs::read(&device.ui.config.input.gb_rom_path[i]).unwrap();
device.transferpaks[i].cart.ram =
std::fs::read(&device.ui.config.input.gb_ram_path[i]).unwrap();
}
}
if device.ui.config.emulation.usb {
(shutdown_tx, device.ui.usb) = ui::usb::init();
}
if let Some(username) = args.ra_username {
retroachievements::init_client(
if cfg!(ra_hardcore_enabled) {
args.ra_hardcore
} else {
false
},
args.ra_challenge,
args.ra_leaderboard,
);
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
if let Some(password) = args.ra_password {
retroachievements::login_user(username, password, tx);
} else if let Some(token) = args.ra_token {
retroachievements::login_token_user(username, token, tx);
} else {
tx.send(false).unwrap();
}
rx.await.unwrap();
}
}
retroachievements::load_game(&rom_contents, rom_contents.len()).await;
device::run_game(
&mut device,
&rom_contents,
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats,
load_savestate_slot: args.load_state,
},
);
retroachievements::shutdown_client();
if device.netplay.is_some() {
netplay::close(&mut device);
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_ram_path[i].is_empty()
{
tokio::fs::write(
&device.ui.config.input.gb_ram_path[i],
&device.transferpaks[i].cart.ram,
)
.await
.unwrap();
}
}
}
if let Some(shutdown_tx) = &shutdown_tx {
ui::usb::close(shutdown_tx);
}
} else if std::env::args().count() > 1 {
let mut ui = ui::Ui::new();
if args.clear_input_bindings {
ui::input::clear_bindings(&mut ui);
return Ok(());
}
if let Some(port) = args.port
&& !(1..=4).contains(&port)
{
return Err(Error::other("Port must be between 1 and 4"));
}
if args.list_controllers {
let controllers = ui::input::get_controller_names(&ui);
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {i}: {controller}");
}
return Ok(());
}
if let Some(profile) = args.configure_input_profile {
ui::input::configure_input_profile(
&mut ui,
profile,
args.use_dinput,
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
);
return Ok(());
}
if let Some(assign_controller) = args.assign_controller {
let Some(port) = args.port else {
return Err(Error::other("Must specify port number"));
};
ui::input::assign_controller(&mut ui, assign_controller, port);
}
if let Some(profile) = args.bind_input_profile {
let Some(port) = args.port else {
return Err(Error::other("Must specify port number"));
};
ui::input::bind_input_profile(&mut ui, profile, port);
}
} else {
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
retroachievements::shutdown_client();
}
}
Ok(())
fn main() -> std::io::Result<()> {
let (close_tx, handle) = gopher64::create_runtime();
let _guard = handle.enter();
let args = gopher64::Args::parse();
let result = gopher64::run(args, std::env::args().count());
close_tx.send(()).unwrap();
result
}
+549 -306
View File
@@ -1,343 +1,586 @@
use crate::{device, ui};
use std::io::{Read, Write};
//UDP packet formats
const UDP_SEND_KEY_INFO: u8 = 0;
const UDP_RECEIVE_KEY_INFO: u8 = 1;
const UDP_REQUEST_KEY_INFO: u8 = 2;
const UDP_RECEIVE_KEY_INFO_GRATUITOUS: u8 = 3;
const UDP_SYNC_DATA: u8 = 4;
use crate::device;
use crate::ui;
use sha2::digest::Digest;
//TCP packet formats
const TCP_SEND_SAVE: u8 = 1;
//const TCP_RECEIVE_SAVE: u8 = 2;
//const TCP_SEND_SETTINGS: u8 = 3;
//const TCP_RECEIVE_SETTINGS: u8 = 4;
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 TCP_SEND_RNG: u8 = 65;
const TCP_RECEIVE_RNG: u8 = 129;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RtcIceServerConfig {
pub urls: Vec<String>,
pub username: Option<String>,
pub credential: Option<String>,
}
const CS4: u32 = 32;
pub struct GgrsConfig;
impl ggrs::Config for GgrsConfig {
type Input = ui::input::InputData;
type InputPredictor = ggrs::PredictRepeatLast;
type State = i32;
type Address = matchbox_socket::PeerId;
}
pub struct MatchboxChannel {
channel: matchbox_socket::WebRtcChannel,
}
impl ggrs::NonBlockingSocket<matchbox_socket::PeerId> for MatchboxChannel {
fn send_to(&mut self, msg: &ggrs::Message, addr: &matchbox_socket::PeerId) {
let encoded = postcard::to_stdvec(msg).expect("serialization failed");
let _ = self.channel.try_send(encoded.into(), *addr);
}
fn receive_all_messages(&mut self) -> Vec<(matchbox_socket::PeerId, ggrs::Message)> {
self.channel
.receive()
.iter()
.filter_map(|(peer, packet)| {
let msg = postcard::from_bytes::<ggrs::Message>(packet).ok()?;
Some((*peer, msg))
})
.collect()
}
}
pub struct NetplayConfig {
pub server_addr: String,
pub player_number: usize,
pub number_of_players: usize,
pub input_delay: usize,
pub ice_config_path: std::path::PathBuf,
}
pub struct Netplay {
udp_socket: std::net::UdpSocket,
tcp_stream: std::net::TcpStream,
pub player_number: u8,
pub player_data: [PlayerData; 4],
vi_counter: u32,
status: u8,
buffer_target: u8,
pub fast_forward: bool,
pub disconnected: bool,
pub session: ggrs::P2PSession<GgrsConfig>,
pub reliable_channel: matchbox_socket::WebRtcChannel,
pub peers: Vec<matchbox_socket::PeerId>,
pub player_number: usize,
pub connected: [bool; 4],
pub input_delay: usize,
pub messages: std::collections::HashMap<String, Vec<u8>>,
pub received_data: std::collections::VecDeque<Vec<u8>>,
pub inputs: Vec<(ui::input::InputData, ggrs::InputStatus)>,
pub requests: std::collections::VecDeque<ggrs::GgrsRequest<GgrsConfig>>,
pub incoming_message: Vec<u8>,
pub ice_config_path: std::path::PathBuf,
}
pub struct PlayerData {
lag: u8,
count: u32,
pub reg_id: u32,
input_events: std::collections::HashMap<u32, InputEvent>,
#[derive(serde::Serialize, serde::Deserialize)]
struct NetplayMessage {
name: String,
data: Vec<u8>,
}
struct InputEvent {
input: u32,
plugin: u8,
fn send_message(netplay: &mut Netplay, message: NetplayMessage) {
let data = postcard::to_stdvec(&message).unwrap();
let chunks = data.chunks(16384).collect::<Vec<&[u8]>>();
for peer in netplay.peers.iter() {
for chunk in chunks.iter() {
if let Err(e) = netplay
.reliable_channel
.try_send(chunk.to_vec().into(), *peer)
{
eprintln!("Failed to send message: {}", e);
}
}
}
}
fn process_reliable_messages(netplay: &mut Netplay) {
netplay.received_data.extend(
netplay
.reliable_channel
.receive()
.iter()
.map(|(_, data)| data.to_vec()),
);
while !netplay.received_data.is_empty() {
if let Some(data) = netplay.received_data.pop_front() {
netplay.incoming_message.extend(data);
if let Ok(decoded_message) =
postcard::from_bytes::<NetplayMessage>(&netplay.incoming_message)
{
netplay
.messages
.insert(decoded_message.name, decoded_message.data);
netplay.incoming_message.clear();
check_input_delay(netplay);
check_disconnect(netplay);
}
}
}
}
fn receive_message(netplay: &mut Netplay, name: &str) -> Vec<u8> {
let timeout = std::time::Duration::from_secs(10);
let now = std::time::Instant::now();
loop {
process_reliable_messages(netplay);
if let Some(data) = netplay.messages.remove(name) {
return data;
}
if now.elapsed() > timeout {
panic!("Could not receive message for {name}");
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
fn send_player_number(
channel: &mut matchbox_socket::WebRtcChannel,
peers: &[matchbox_socket::PeerId],
player_number: usize,
) {
let message = NetplayMessage {
name: "player_number".to_string(),
data: (player_number as u64).to_be_bytes().to_vec(),
};
let data = postcard::to_stdvec(&message).unwrap();
for peer in peers {
if let Err(e) = channel.try_send(data.clone().into(), *peer) {
eprintln!("Failed to send message: {}", e);
}
}
}
fn get_player_numbers(
channel: &mut matchbox_socket::WebRtcChannel,
player_numbers: &mut std::collections::BTreeMap<usize, Option<matchbox_socket::PeerId>>,
) {
for (peer, data) in channel.receive() {
let message = postcard::from_bytes::<NetplayMessage>(&data).unwrap();
if message.name == "player_number" {
player_numbers.insert(
u64::from_be_bytes(message.data.try_into().unwrap()) as usize,
Some(peer),
);
}
}
}
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();
let message = NetplayMessage {
name: "rtc".to_string(),
data: rtc.to_be_bytes().to_vec(),
};
send_message(netplay, message);
}
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)
let message = receive_message(netplay, "rtc");
i64::from_be_bytes(message.try_into().unwrap())
}
pub fn send_rng(netplay: &mut Netplay, seed: u64) {
let mut request: Vec<u8> = [TCP_SEND_RNG].to_vec();
let size: u32 = 8;
request.extend_from_slice(&size.to_be_bytes());
request.extend_from_slice(&seed.to_be_bytes());
netplay.tcp_stream.write_all(&request).unwrap();
let message = NetplayMessage {
name: "rng".to_string(),
data: seed.to_be_bytes().to_vec(),
};
send_message(netplay, message);
}
pub fn receive_rng(netplay: &mut Netplay) -> u64 {
let request: Vec<u8> = [TCP_RECEIVE_RNG].to_vec();
netplay.tcp_stream.write_all(&request).unwrap();
let mut seed: [u8; 8] = [0; 8];
netplay.tcp_stream.read_exact(&mut seed).unwrap();
u64::from_be_bytes(seed)
let message = receive_message(netplay, "rng");
u64::from_be_bytes(message.try_into().unwrap())
}
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());
request.push(0); // null terminate string
request.extend_from_slice(&(size as u32).to_be_bytes());
if size > 0 {
request.extend_from_slice(save_data);
}
netplay.tcp_stream.write_all(&request).unwrap();
pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8]) {
let message = NetplayMessage {
name: save_type.to_string(),
data: save_data.to_vec(),
};
send_message(netplay, message);
}
pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<u8>) {
let mut request: Vec<u8> = [TCP_RECEIVE_SAVE_WITH_SIZE].to_vec();
request.extend_from_slice(save_type.as_bytes());
request.push(0); // null terminate string
netplay.tcp_stream.write_all(&request).unwrap();
let mut size: [u8; 4] = [0; 4];
netplay.tcp_stream.read_exact(&mut size).unwrap();
let mut response: Vec<u8> = vec![0; u32::from_be_bytes(size) as usize];
netplay.tcp_stream.read_exact(&mut response).unwrap();
*save_data = response;
let message = receive_message(netplay, save_type);
*save_data = message;
}
pub fn send_sync_check(netplay: &mut Netplay, regs: &[u64]) {
if netplay.vi_counter.is_multiple_of(600) {
let mut request: Vec<u8> = [UDP_SYNC_DATA].to_vec();
request.extend_from_slice(&(netplay.vi_counter).to_be_bytes());
for item in regs.iter() {
request.extend_from_slice(&(*item as u32).to_be_bytes());
}
netplay.udp_socket.send(&request).unwrap();
}
netplay.vi_counter = netplay.vi_counter.wrapping_add(1);
}
pub fn send_input(netplay: &Netplay, input: ui::input::InputData) {
let mut request: Vec<u8> = [UDP_SEND_KEY_INFO].to_vec();
request.push(netplay.player_number);
request.extend_from_slice(
&(netplay.player_data[netplay.player_number as usize].count).to_be_bytes(),
);
request.extend_from_slice(&(input.data).to_be_bytes());
request.push(input.pak_change_pressed as u8);
netplay.udp_socket.send(&request).unwrap();
}
pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::InputData {
let netplay = device.netplay.as_mut().unwrap();
let mut input = None;
let timeout = std::time::Instant::now() + std::time::Duration::from_secs(10);
let mut request_timer = std::time::Instant::now() - std::time::Duration::from_millis(5);
while input.is_none() {
process_incoming(netplay); // we execute process_incoming before request_input so that we send an accurate buffer count
if std::time::Instant::now() > request_timer {
// sends a request packet every 5ms
request_input(netplay, channel);
request_timer = std::time::Instant::now() + std::time::Duration::from_millis(5);
}
input = netplay.player_data[channel]
.input_events
.remove(&netplay.player_data[channel].count);
if std::time::Instant::now() > timeout {
ui::video::onscreen_message("Lost connection to netplay server", false);
input = Some(InputEvent {
input: 0,
plugin: 0,
});
device.cpu.running = false;
}
pub fn send_input_delay(netplay: &mut Netplay, input_delay: usize) {
if input_delay < 1 {
return;
}
netplay.fast_forward = netplay.player_data[channel].lag > 0
&& netplay.player_data[channel].input_events.len() as u8 > netplay.buffer_target;
netplay.player_data[channel].count = netplay.player_data[channel].count.wrapping_add(1);
ui::input::InputData {
data: input.as_ref().unwrap().input,
pak_change_pressed: input.as_ref().unwrap().plugin != 0,
}
}
fn request_input(netplay: &Netplay, channel: usize) {
let mut request: Vec<u8> = [UDP_REQUEST_KEY_INFO].to_vec();
request.push(channel as u8); //The player we need input for
request.extend_from_slice(
&(netplay.player_data[netplay.player_number as usize].reg_id).to_be_bytes(),
);
request.extend_from_slice(&(netplay.player_data[channel].count).to_be_bytes());
request.push(0); //spectator mode
request.push(netplay.player_data[channel].input_events.len() as u8);
netplay.udp_socket.send(&request).unwrap();
}
fn process_incoming(netplay: &mut Netplay) {
let mut buf: [u8; 1024] = [0; 1024];
while let Ok(_incoming) = netplay.udp_socket.recv(&mut buf) {
match buf[0] {
UDP_RECEIVE_KEY_INFO | UDP_RECEIVE_KEY_INFO_GRATUITOUS => {
let player = buf[1] as usize;
//current_status is a status update from the server
//it will let us know if another player has disconnected, or the games have desynced
let current_status = buf[2];
if buf[0] == UDP_RECEIVE_KEY_INFO {
netplay.player_data[player].lag = buf[3];
}
if current_status != netplay.status {
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
ui::video::onscreen_message("Netplay desync detected", true);
}
for dis in 1..5 {
if ((current_status & (0x1 << dis)) ^ (netplay.status & (0x1 << dis))) != 0
{
ui::video::onscreen_message(
&format!("Player {dis} disconnected"),
false,
);
}
}
netplay.status = current_status;
}
let mut buffer_offset = 5;
for _i in 0..buf[4] {
let count = u32::from_be_bytes(
buf[buffer_offset..buffer_offset + 4].try_into().unwrap(),
);
buffer_offset += 4;
if (count.wrapping_sub(netplay.player_data[player].count)) > (u32::MAX / 2) {
//event doesn't need to be recorded
buffer_offset += 5;
continue;
}
let input = u32::from_be_bytes(
buf[buffer_offset..buffer_offset + 4].try_into().unwrap(),
);
buffer_offset += 4;
let plugin = buf[buffer_offset];
buffer_offset += 1;
let input_event = InputEvent { input, plugin };
netplay.player_data[player]
.input_events
.insert(count, input_event);
}
}
_ => {
panic!("unknown UDP packet")
}
}
}
}
pub fn init(peer_addr: std::net::SocketAddr, player_number: u8) -> Netplay {
let udp_socket;
if peer_addr.is_ipv4() {
udp_socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.expect("couldn't bind to address");
socket2::SockRef::from(&udp_socket)
.set_tos_v4(CS4 << 2)
.unwrap();
} else {
udp_socket = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0))
.expect("couldn't bind to address");
#[cfg(not(target_os = "windows"))]
socket2::SockRef::from(&udp_socket)
.set_tclass_v6(CS4 << 2)
.unwrap();
let message = NetplayMessage {
name: "input_delay".to_string(),
data: (input_delay as u64).to_be_bytes().to_vec(),
};
send_message(netplay, message);
change_input_delay(netplay, input_delay);
}
udp_socket.connect(peer_addr).unwrap();
udp_socket.set_nonblocking(true).unwrap();
let mut stream = std::net::TcpStream::connect(peer_addr).unwrap();
let regid = (player_number + 1) as u32;
let mut request: [u8; 8] = [
TCP_REGISTER_PLAYER,
player_number,
0, //plugin/pak
0, //rawdata
0, //regid (u32)
0, //regid (u32)
0, //regid (u32)
0, //regid (u32)
];
request[4..8].copy_from_slice(&regid.to_be_bytes());
stream.write_all(&request).unwrap();
let mut response: [u8; 2] = [0, 0];
stream.read_exact(&mut response).unwrap();
if response[0] != 1 {
panic!("Failed to register player");
}
let buffer_target = response[1];
let request: [u8; 1] = [TCP_GET_REGISTRATION];
stream.write_all(&request).unwrap();
let mut response: [u8; 24] = [0; 24];
stream.read_exact(&mut response).unwrap();
let mut reg_id: [u32; 4] = [0; 4];
for i in 0..4 {
// reg_id of 0 means no player connected
reg_id[i] = u32::from_be_bytes(response[(i * 6)..(i * 6) + 4].try_into().unwrap());
}
Netplay {
udp_socket,
tcp_stream: stream,
player_number,
vi_counter: 0,
status: 0,
buffer_target,
fast_forward: false,
player_data: [
PlayerData {
lag: 0,
count: 0,
reg_id: reg_id[0],
input_events: std::collections::HashMap::new(),
},
PlayerData {
lag: 0,
count: 0,
reg_id: reg_id[1],
input_events: std::collections::HashMap::new(),
},
PlayerData {
lag: 0,
count: 0,
reg_id: reg_id[2],
input_events: std::collections::HashMap::new(),
},
PlayerData {
lag: 0,
count: 0,
reg_id: reg_id[3],
input_events: std::collections::HashMap::new(),
},
],
fn change_input_delay(netplay: &mut Netplay, input_delay: usize) {
netplay.input_delay = input_delay;
for handle in netplay.session.local_player_handles() {
if let Err(e) = netplay.session.set_input_delay(handle, input_delay) {
eprintln!("Error setting input delay: {}", e);
} else {
ui::video::onscreen_message(
&format!("Input delay: {}", input_delay),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
}
}
pub fn close(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
let regid = (netplay.player_number + 1) as u32;
let mut request: [u8; 5] = [TCP_DISCONNECT_NOTICE, 0, 0, 0, 0];
request[1..5].copy_from_slice(&regid.to_be_bytes());
fn check_input_delay(netplay: &mut Netplay) {
if let Some(data) = netplay.messages.remove("input_delay") {
let input_delay = u64::from_be_bytes(data.try_into().unwrap()) as usize;
if input_delay != netplay.input_delay {
change_input_delay(netplay, input_delay);
}
}
}
netplay.tcp_stream.write_all(&request).unwrap();
netplay.tcp_stream.flush().unwrap();
fn check_disconnect(netplay: &mut Netplay) {
if !netplay.disconnected && netplay.messages.remove("disconnect").is_some() {
netplay.disconnected = true;
ui::video::onscreen_message(
"Player disconnected, session has ended",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
}
}
fn pending_frames(netplay: &Netplay) -> usize {
netplay
.tcp_stream
.shutdown(std::net::Shutdown::Both)
.unwrap();
.requests
.iter()
.filter(|r| matches!(r, ggrs::GgrsRequest::AdvanceFrame { .. }))
.count()
}
pub fn in_rollback(netplay: Option<&Netplay>) -> bool {
if let Some(netplay) = netplay {
pending_frames(netplay) != 0
} else {
false
}
}
pub fn process_requests(
device: &mut device::Device,
) -> Vec<(ui::input::InputData, ggrs::InputStatus)> {
loop {
if let Some(request) = device.netplay.as_mut().unwrap().requests.pop_front() {
match request {
ggrs::GgrsRequest::SaveGameState { cell, frame } => {
//savestates::create_savestate(device, true, Some(frame));
let mut hasher = sha2::Sha256::new();
for reg in device.cpu.cop0.regs.as_ref() {
hasher.update(reg.to_be_bytes());
}
let hash = u128::from_be_bytes(hasher.finalize()[..16].try_into().unwrap());
cell.save(frame, Some(frame), Some(hash));
}
ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => {
eprintln!("attempting to load game state");
if let Some(_frame) = cell.load() {
// savestates::load_savestate(device, true, Some(frame));
}
}
ggrs::GgrsRequest::AdvanceFrame { inputs } => {
return inputs;
}
}
} else {
process_netplay(device);
}
}
}
fn poll_clients(netplay: &mut Netplay) {
netplay.session.poll_remote_clients();
for event in netplay.session.events() {
match event {
ggrs::GgrsEvent::Synchronizing { .. } => {}
ggrs::GgrsEvent::Synchronized { .. } => {}
ggrs::GgrsEvent::Disconnected { .. } => {
if !netplay.disconnected {
netplay.disconnected = true;
ui::video::onscreen_message(
"Lost connection to peer(s)",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
}
}
ggrs::GgrsEvent::NetworkInterrupted { .. } => {
println!("network interrupted");
}
ggrs::GgrsEvent::NetworkResumed { .. } => {
println!("network resumed");
}
ggrs::GgrsEvent::WaitRecommendation { skip_frames } => {
println!("wait recommendation: skip_frames={}", skip_frames);
}
ggrs::GgrsEvent::DesyncDetected { .. } => {
eprintln!("desync detected");
ui::video::onscreen_message(
"Desync detected",
ui::video::MESSAGE_LENGTH_MESSAGE_LONG,
);
}
}
}
}
fn process_netplay(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
poll_clients(netplay);
process_reliable_messages(netplay);
advance_frame(device);
}
fn advance_frame(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
let local_input = if netplay.session.current_frame() > netplay.session.max_prediction() as i32 {
ui::input::get(&mut device.ui, 0)
} else {
//workaround for disabled rollback
ui::input::InputData::default()
};
let local_handle = *netplay.session.local_player_handles().first().unwrap();
netplay
.session
.add_local_input(local_handle, local_input)
.unwrap();
// avoid rollback
while !netplay.disconnected
&& netplay.session.current_frame() > netplay.session.confirmed_frame()
&& netplay.session.confirmed_frame() != ggrs::NULL_FRAME
{
poll_clients(netplay);
}
if netplay.disconnected {
netplay.requests.push_back(ggrs::GgrsRequest::AdvanceFrame {
inputs: vec![
(
ui::input::InputData::default(),
ggrs::InputStatus::Disconnected
);
4
],
});
return;
}
match netplay.session.advance_frame() {
Ok(requests) => {
netplay.requests.extend(requests);
}
Err(ggrs::GgrsError::PredictionThreshold) => {
println!("prediction threshold reached");
}
Err(e) => panic!("{e}"),
}
}
fn verify_peers(
peers: &[matchbox_socket::PeerId],
player_numbers: &std::collections::BTreeMap<usize, Option<matchbox_socket::PeerId>>,
) -> bool {
for peer in player_numbers.values() {
if let Some(peer) = peer
&& !peers.contains(peer)
{
return false;
}
}
true
}
fn create_socket(builder: matchbox_socket::WebRtcSocketBuilder) -> matchbox_socket::WebRtcSocket {
let (socket, loop_fut) = builder.build();
tokio::spawn(async move {
if let Err(e) = loop_fut.await {
eprintln!("WebRTC loop failed: {}", e);
}
});
socket
}
pub fn init(
device: &mut device::Device,
netplay_config: &NetplayConfig,
pal: bool,
) -> Option<Netplay> {
let mut builder = matchbox_socket::WebRtcSocketBuilder::new(&netplay_config.server_addr)
.add_unreliable_channel()
.add_reliable_channel();
if let Ok(ice_config) = std::fs::read(&netplay_config.ice_config_path)
&& let Ok(ice_config) = serde_json::from_slice::<RtcIceServerConfig>(&ice_config)
{
builder = builder.ice_server(matchbox_socket::RtcIceServerConfig {
urls: ice_config.urls,
username: ice_config.username,
credential: ice_config.credential,
});
} else {
eprintln!("Using default ICE config");
}
let mut socket = create_socket(builder.clone());
let mut now = std::time::Instant::now();
let mut message_timer = now;
let socket_timeout = std::time::Duration::from_secs_f64(rand::random_range(8.0..10.0));
let mut player_numbers =
std::collections::BTreeMap::from([(netplay_config.player_number, None)]);
ui::video::onscreen_message(
"Connecting to netplay peers...\nPlease wait...",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
device.cpu.running = true;
while device.cpu.running {
if socket
.update_peers()
.iter()
.any(|(_, peer_state)| *peer_state == matchbox_socket::PeerState::Disconnected)
{
// if someone has disconnected, reset the timeout
now = std::time::Instant::now();
}
let connected_peers = socket
.connected_peers()
.collect::<Vec<matchbox_socket::PeerId>>();
if connected_peers.len() == netplay_config.number_of_players - 1 {
send_player_number(
socket.channel_mut(1),
&connected_peers,
netplay_config.player_number,
);
get_player_numbers(socket.channel_mut(1), &mut player_numbers);
if player_numbers.len() == netplay_config.number_of_players
&& verify_peers(&connected_peers, &player_numbers)
{
break;
}
} else if now.elapsed() > socket_timeout {
socket.close();
player_numbers.retain(|_, peer| peer.is_none());
socket = create_socket(builder.clone());
now = std::time::Instant::now();
}
if message_timer.elapsed() > std::time::Duration::from_secs(4) {
ui::video::onscreen_message(
&format!(
"Still connecting to {} netplay peer(s)...\nPlease wait...",
netplay_config.number_of_players - player_numbers.len()
),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
message_timer = std::time::Instant::now();
}
ui::video::render_frame();
ui::video::update_screen();
std::thread::sleep(std::time::Duration::from_millis(10));
ui::video::check_callback(device);
}
if !device.cpu.running {
// user closed the window
return None;
}
device.cpu.running = false;
let mut session_builder = ggrs::SessionBuilder::<GgrsConfig>::new()
.with_num_players(netplay_config.number_of_players)
.unwrap()
.with_input_delay(netplay_config.input_delay)
.with_fps(if pal { 50 } else { 60 })
.unwrap()
.with_desync_detection_mode(ggrs::DesyncDetection::On { interval: 60 })
.with_max_prediction_window(16)
.with_disconnect_timeout(std::time::Duration::from_secs(if cfg!(debug_assertions) {
10
} else {
5
}));
let mut peers = vec![];
for (i, peer) in player_numbers.iter() {
if let Some(peer) = peer {
session_builder = session_builder
.add_player(ggrs::PlayerType::Remote(*peer), *i)
.unwrap();
peers.push(*peer);
} else {
session_builder = session_builder
.add_player(ggrs::PlayerType::Local, *i)
.unwrap();
}
}
let matchbox_channel = MatchboxChannel {
channel: socket.take_channel(0).unwrap(),
};
let reliable_channel = socket.take_channel(1).unwrap();
if matchbox_channel.channel.config().max_retransmits != Some(0)
|| matchbox_channel.channel.config().ordered
{
eprintln!("Sending GGRS traffic over reliable channel");
}
let mut session = session_builder.start_p2p_session(matchbox_channel).unwrap();
let now = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
while session.current_state() != ggrs::SessionState::Running {
session.poll_remote_clients();
if now.elapsed() > timeout {
eprintln!("Could not start netplay session");
return None;
}
unsafe { sdl3_sys::events::SDL_PumpEvents() };
std::thread::sleep(std::time::Duration::from_millis(10));
}
Some(Netplay {
disconnected: false,
incoming_message: vec![],
input_delay: netplay_config.input_delay,
session,
reliable_channel,
peers,
player_number: netplay_config.player_number,
connected: [
netplay_config.number_of_players > 0,
netplay_config.number_of_players > 1,
netplay_config.number_of_players > 2,
netplay_config.number_of_players > 3,
],
inputs: Vec::new(),
requests: std::collections::VecDeque::new(),
received_data: std::collections::VecDeque::new(),
messages: std::collections::HashMap::new(),
ice_config_path: netplay_config.ice_config_path.clone(),
})
}
pub fn close(netplay: &mut Netplay) {
if !netplay.disconnected {
let message = NetplayMessage {
name: "disconnect".to_string(),
data: vec![],
};
send_message(netplay, message);
std::thread::sleep(std::time::Duration::from_millis(200)); // give the message time to be sent
}
let _ = std::fs::remove_file(&netplay.ice_config_path);
}
+121 -35
View File
@@ -2,18 +2,9 @@ include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
use crate::ui;
static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
reqwest::Client::builder()
.user_agent(format!(
"{}/{}",
env!("CARGO_PKG_NAME"),
env!("GIT_DESCRIBE")
))
.build()
.unwrap()
});
use discord_rich_presence::DiscordIpc;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize, Default)]
pub struct RAConfig {
pub username: String,
pub token: String,
@@ -21,6 +12,7 @@ pub struct RAConfig {
pub hardcore: bool,
pub challenge: bool,
pub leaderboard: bool,
pub rich_presence: bool,
}
#[unsafe(no_mangle)]
@@ -53,6 +45,7 @@ pub extern "C" fn store_retroachievements_credentials(
hardcore: result.hardcore,
challenge: result.challenge,
leaderboard: result.leaderboard,
rich_presence: result.rich_presence,
}
} else {
RAConfig {
@@ -62,6 +55,7 @@ pub extern "C" fn store_retroachievements_credentials(
hardcore: false,
challenge: false,
leaderboard: false,
rich_presence: false,
}
};
let f = std::fs::File::create(&file_path).unwrap();
@@ -85,13 +79,13 @@ pub extern "C" fn rust_server_call(
unsafe { std::ffi::CStr::from_ptr(c_post_data).to_str().unwrap() }.to_string();
let content_type =
unsafe { std::ffi::CStr::from_ptr(c_content_type).to_str().unwrap() }.to_string();
WEB_CLIENT
ui::WEB_CLIENT
.post(url)
.body(post_data)
.header(reqwest::header::CONTENT_TYPE, content_type)
.send()
} else {
WEB_CLIENT.get(url).send()
ui::WEB_CLIENT.get(url).send()
};
let callback = c_callback.addr();
let callback_data = c_callback_data.addr();
@@ -143,25 +137,83 @@ pub extern "C" fn rust_server_call(
});
}
pub async fn load_game(rom: &[u8], rom_size: usize) {
pub fn load_game(
rom: &[u8],
rom_size: usize,
discord_rich_presence: bool,
) -> (
Option<tokio::sync::watch::Sender<()>>,
Option<tokio::task::JoinHandle<()>>,
) {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
ra_load_game(rom.as_ptr(), rom_size, tx_ptr);
};
rx.await.unwrap();
rx.blocking_recv().unwrap();
let mut c_title = std::ptr::null();
let mut c_image_url = std::ptr::null();
unsafe { ra_welcome(&mut c_title, &mut c_image_url) };
if !discord_rich_presence || c_title.is_null() || c_image_url.is_null() {
(None, None)
} else {
let (discord_watch_tx, discord_watch_rx) = tokio::sync::watch::channel(());
(
Some(discord_watch_tx),
Some(init_rich_presence(
discord_watch_rx,
unsafe { std::ffi::CStr::from_ptr(c_title) }
.to_str()
.unwrap()
.to_string(),
unsafe { std::ffi::CStr::from_ptr(c_image_url) }
.to_str()
.unwrap()
.to_string(),
)),
)
}
}
pub fn unload_game() {
pub fn unload_game(
discord_watch_tx: Option<tokio::sync::watch::Sender<()>>,
discord_handle: Option<tokio::task::JoinHandle<()>>,
) {
if let Some(mut discord_handle) = discord_handle
&& let Some(discord_watch_tx) = discord_watch_tx
{
let _ = discord_watch_tx.send(());
tokio::task::spawn(async move {
if tokio::time::timeout(std::time::Duration::from_secs(1), &mut discord_handle)
.await
.is_err()
{
discord_handle.abort();
}
});
}
unsafe { ra_unload_game() };
}
pub fn welcome() {
unsafe { ra_welcome() };
pub fn get_rich_presence() -> Option<String> {
let c_rich_presence = unsafe { ra_get_rich_presence() };
if c_rich_presence.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(c_rich_presence) }
.to_str()
.unwrap()
.to_string(),
)
}
}
pub fn set_dmem(dmem: *const u8, dmem_size: usize) {
unsafe { ra_set_dmem(dmem, dmem_size) };
pub fn set_rdram(rdram: *const u8, rdram_size: usize) {
unsafe { ra_set_rdram(rdram, rdram_size) };
}
pub fn do_frame() {
@@ -222,21 +274,6 @@ pub fn get_username() -> Option<String> {
}
}
#[cfg(feature = "gui")]
pub fn get_token() -> Option<String> {
let c_token = unsafe { ra_get_token() };
if c_token.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(c_token) }
.to_str()
.unwrap()
.to_string(),
)
}
}
pub fn state_size() -> usize {
unsafe { ra_state_size() }
}
@@ -248,3 +285,52 @@ pub fn save_state(state: *mut u8, state_size: usize) {
pub fn load_state(state: *const u8, state_size: usize) {
unsafe { ra_load_state(state, state_size) };
}
pub fn init_rich_presence(
mut discord_watch_rx: tokio::sync::watch::Receiver<()>,
game_title: String,
game_image_url: String,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut client = discord_rich_presence::DiscordIpcClient::new("1395482226463870986");
let timestamps = discord_rich_presence::activity::Timestamps::new().start(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64,
);
if let Err(e) = client.connect() {
eprintln!("Failed to connect to Discord: {e}");
return;
}
loop {
tokio::select! {
_ = discord_watch_rx.changed() => {
if let Err(e) = client.clear_activity() {
eprintln!("Failed to clear Discord activity: {e}");
}
if let Err(e) = client.close() {
eprintln!("Failed to close Discord: {e}");
}
return;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
if let Some(rich_presence) = get_rich_presence()
&& let Err(e) = client.set_activity(
discord_rich_presence::activity::Activity::new()
.details(game_title.clone())
.state(rich_presence)
.assets(
discord_rich_presence::activity::Assets::new()
.small_image(game_image_url.clone()),
).timestamps(timestamps.clone()),
)
{
eprintln!("Failed to set Discord activity: {e}");
}
}
}
}
})
}
+289 -134
View File
@@ -5,6 +5,7 @@ use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
use serde::ser::{Serialize, SerializeSeq, Serializer};
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
use std::ops::Deref;
struct M128iArrayVisitor<const N: usize>;
@@ -69,172 +70,326 @@ where
seq.end()
}
pub fn create_savestate(device: &device::Device) {
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Savestate {
pub save_state: bool,
pub load_state: bool,
pub load_rewind: bool,
pub last_rewind_saved: f64,
#[serde(skip)]
pub rewind_pool:
std::sync::Arc<tokio::sync::Mutex<std::collections::BTreeMap<i32, SavestateData>>>,
}
pub struct SavestateData {
device: Box<device::Device>,
saves: ui::storage::Saves,
rdp_state: Vec<u8>,
ra_state: Vec<u8>,
}
static DEVICE_CLONE: std::sync::LazyLock<tokio::sync::Mutex<Box<device::Device>>> =
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(device::Device::new(false)));
static SAVES_CLONE: std::sync::LazyLock<tokio::sync::Mutex<ui::storage::Saves>> =
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(ui::storage::Saves::default()));
pub fn process_savestates(device: &mut device::Device) {
let save_rewind = if device.netplay.is_none()
&& device.ui.config.emulation.rewind
&& device.vi.elapsed_time - device.savestate.last_rewind_saved > 1.0
{
device.savestate.last_rewind_saved = device.vi.elapsed_time;
true
} else {
false
};
if device.savestate.save_state || save_rewind {
create_savestate(device, !device.savestate.save_state, None);
device.savestate.save_state = false;
} else if device.savestate.load_state || device.savestate.load_rewind {
load_savestate(device, device.savestate.load_rewind, None);
device.savestate.load_state = false;
device.savestate.load_rewind = false;
}
}
pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame: Option<i32>) {
if !rewind {
ui::video::idle();
}
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
ui::video::save_state(rdp_state.as_mut_ptr());
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
let data: &[(&[u8], &str)] = &[
(&postcard::to_stdvec(device).unwrap(), "device"),
(
&postcard::to_stdvec(&device.ui.storage.saves).unwrap(),
"saves",
),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
];
DEVICE_CLONE.blocking_lock().clone_state(device);
SAVES_CLONE
.blocking_lock()
.clone_from(&device.ui.storage.saves);
let save_path = device.ui.storage.paths.savestate_file_path.clone();
let compressed_file = ui::storage::compress_file(data);
let save_state_slot = device.ui.storage.save_state_slot;
let rewind_pool = device.savestate.rewind_pool.clone();
tokio::spawn(async move {
tokio::fs::write(save_path, compressed_file).await.unwrap();
let mut error = false;
if rewind {
let mut pool = rewind_pool.lock().await;
let mut state = device::Device::new(false);
state.clone_state(DEVICE_CLONE.lock().await.deref());
let key = if let Some(key) = rewind_frame {
key
} else if let Some(key) = pool.keys().last() {
key + 1
} else {
0
};
pool.insert(
key,
SavestateData {
device: state,
saves: SAVES_CLONE.lock().await.clone(),
rdp_state,
ra_state,
},
);
if pool.len() > 30 {
pool.pop_first();
}
} else {
let compressed_file = if let Ok(device_data) =
postcard::to_stdvec(DEVICE_CLONE.lock().await.deref())
&& let Ok(saves_data) = postcard::to_stdvec(SAVES_CLONE.lock().await.deref())
&& let Ok(compressed_file) = ui::storage::compress_file(&[
(&device_data, "device"),
(&saves_data, "saves"),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
]) {
Some(compressed_file)
} else {
None
};
if let Some(compressed_file) = compressed_file {
if let Err(e) = tokio::fs::write(save_path, compressed_file).await {
eprintln!("Error writing savestate: {}", e);
error = true;
}
} else {
error = true;
eprintln!("Error compressing savestate");
}
}
if error {
ui::video::onscreen_message(
&format!("Failed to create savestate in slot {}", save_state_slot),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else if !rewind {
ui::video::onscreen_message(
&format!("Savestate created in slot {}", save_state_slot),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
});
ui::video::onscreen_message(
&format!(
"Savestate created in slot {}",
device.ui.storage.save_state_slot
),
false,
);
}
pub fn load_savestate(device: &mut device::Device) {
pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: Option<i32>) {
if retroachievements::get_hardcore() {
ui::video::onscreen_message("Cannot load savestate in RA hardcore mode", false);
ui::video::onscreen_message(
"Cannot load savestate in RA hardcore mode",
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
return;
}
let savestate = std::fs::read(&device.ui.storage.paths.savestate_file_path);
if let Ok(savestate) = &savestate {
let device_bytes = ui::storage::decompress_file(savestate, "device");
let save_bytes = ui::storage::decompress_file(savestate, "saves");
let rdp_state = ui::storage::decompress_file(savestate, "rdp_state");
let ra_state = ui::storage::decompress_file(savestate, "ra_state");
if let Ok(state) = postcard::from_bytes::<device::Device>(&device_bytes) {
device.ui.storage.saves = postcard::from_bytes(&save_bytes).unwrap();
device.cpu = state.cpu;
device.pif = state.pif;
let rom = device.cart.rom.clone();
device.cart = state.cart;
device.cart.rom = rom;
device.memory = state.memory;
device.rsp = state.rsp;
device.rdp = state.rdp;
device.rdram = state.rdram;
device.mi = state.mi;
device.pi = state.pi;
device.vi = state.vi;
device.ai = state.ai;
device.si = state.si;
device.ri = state.ri;
device.vru = state.vru;
device.cheats = state.cheats;
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
for (i, item) in tpak_rom.iter_mut().enumerate() {
*item = device.transferpaks[i].cart.rom.clone();
let state_data = if rewind {
if let Some(rewind_frame) = rewind_frame {
let timeout = std::time::Duration::from_secs(1);
let now = std::time::Instant::now();
loop {
if let Some(state) = device
.savestate
.rewind_pool
.blocking_lock()
.remove(&rewind_frame)
{
break Some(state);
}
if now.elapsed() > timeout {
break None;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
device.transferpaks = state.transferpaks;
for (i, item) in tpak_rom.iter().enumerate() {
device.transferpaks[i].cart.rom = item.clone();
} else if let Some((_key, state)) = device.savestate.rewind_pool.blocking_lock().pop_last()
{
Some(state)
} else {
None
}
} else if let Ok(savestate) = std::fs::read(&device.ui.storage.paths.savestate_file_path)
&& let Ok(device_data) = ui::storage::decompress_file(&savestate, "device")
&& let Ok(saves_data) = ui::storage::decompress_file(&savestate, "saves")
&& let Ok(rdp_state) = ui::storage::decompress_file(&savestate, "rdp_state")
&& let Ok(ra_state) = ui::storage::decompress_file(&savestate, "ra_state")
&& let Ok(state) = postcard::from_bytes::<Box<device::Device>>(&device_data)
&& let Ok(saves) = postcard::from_bytes(&saves_data)
{
Some(SavestateData {
device: state,
saves,
rdp_state,
ra_state,
})
} else {
None
};
if let Some(mut state) = state_data
&& device.rdram.size == state.device.rdram.size
{
if device.netplay.is_none() {
ui::video::idle();
}
device.savestate.last_rewind_saved = state.device.vi.elapsed_time;
std::mem::swap(&mut device.rng, &mut state.device.rng);
std::mem::swap(&mut device.ui.storage.saves, &mut state.saves);
std::mem::swap(&mut device.cpu, &mut state.device.cpu);
std::mem::swap(&mut device.pif, &mut state.device.pif);
std::mem::swap(&mut device.cart, &mut state.device.cart);
std::mem::swap(&mut device.cart.rom, &mut state.device.cart.rom); // ROM is not included in the savestate
std::mem::swap(&mut device.memory, &mut state.device.memory);
std::mem::swap(&mut device.rsp, &mut state.device.rsp);
std::mem::swap(&mut device.rdp, &mut state.device.rdp);
device.rdram.mem.copy_from_slice(&state.device.rdram.mem); // RDRAM address should not change
std::mem::swap(&mut device.rdram.regs, &mut state.device.rdram.regs);
std::mem::swap(&mut device.mi, &mut state.device.mi);
std::mem::swap(&mut device.pi, &mut state.device.pi);
std::mem::swap(&mut device.vi, &mut state.device.vi);
std::mem::swap(&mut device.ai, &mut state.device.ai);
std::mem::swap(&mut device.si, &mut state.device.si);
std::mem::swap(&mut device.ri, &mut state.device.ri);
std::mem::swap(&mut device.vru, &mut state.device.vru);
std::mem::swap(&mut device.cheats, &mut state.device.cheats);
std::mem::swap(&mut device.transferpaks, &mut state.device.transferpaks);
for (i, item) in device.transferpaks.iter_mut().enumerate() {
std::mem::swap(
&mut item.cart.rom,
&mut state.device.transferpaks[i].cart.rom,
); // ROM is not included in the savestate
}
device::memory::init(device);
device::vi::set_expected_refresh_rate(device);
device::cpu::map_instructions(device);
device::cop0::map_instructions(device);
device::cop1::map_instructions(device);
device::cop2::map_instructions(device);
device::rsp_cpu::map_instructions(device);
let mut mem_addr = 0x1000;
while mem_addr < 0x2000 {
let data =
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
device::rsp_cpu::decode_opcode(device, data);
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
mem_addr += 4;
}
for line_index in 0..512 {
for i in 0..8 {
device.memory.icache[line_index].instruction[i] =
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[i]);
}
}
device::memory::init(device);
device::vi::set_expected_refresh_rate(device);
device::cpu::map_instructions(device);
device::cop0::map_instructions(device);
device::cop1::map_instructions(device);
device::cop2::map_instructions(device);
device::rsp_cpu::map_instructions(device);
let mut mem_addr = 0x1000;
while mem_addr < 0x2000 {
let data =
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
device::rsp_cpu::decode_opcode(device, data);
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
mem_addr += 4;
}
for line_index in 0..512 {
for i in 0..8 {
device.memory.icache[line_index].instruction[i] = device::cpu::decode_opcode(
device,
device.memory.icache[line_index].words[i],
);
device::pif::connect_pif_channels(device);
for i in 0..4 {
if let Some(handler) = device.pif.channels[i].pak_handler {
if handler.pak_type == device::controller::PakType::RumblePak {
let rumblepak_handler = device::controller::PakHandler {
read: device::controller::rumble::read,
write: device::controller::rumble::write,
pak_type: device::controller::PakType::RumblePak,
};
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
} else if handler.pak_type == device::controller::PakType::MemPak {
let mempak_handler = device::controller::PakHandler {
read: device::controller::mempak::read,
write: device::controller::mempak::write,
pak_type: device::controller::PakType::MemPak,
};
device.pif.channels[i].pak_handler = Some(mempak_handler);
} else if handler.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);
}
}
}
device::pif::connect_pif_channels(device);
for i in 0..4 {
if let Some(handler) = device.pif.channels[i].pak_handler {
if handler.pak_type == device::controller::PakType::RumblePak {
let rumblepak_handler = device::controller::PakHandler {
read: device::controller::rumble::read,
write: device::controller::rumble::write,
pak_type: device::controller::PakType::RumblePak,
};
device.pif.channels[i].pak_handler = Some(rumblepak_handler);
} else if handler.pak_type == device::controller::PakType::MemPak {
let mempak_handler = device::controller::PakHandler {
read: device::controller::mempak::read,
write: device::controller::mempak::write,
pak_type: device::controller::PakType::MemPak,
};
device.pif.channels[i].pak_handler = Some(mempak_handler);
} else if handler.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);
}
}
}
ui::audio::update_freq(device);
ui::video::load_state(device, state.rdp_state.as_ptr());
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq);
ui::video::load_state(device, rdp_state.as_ptr());
if !state.ra_state.is_empty() {
retroachievements::load_state(state.ra_state.as_ptr(), state.ra_state.len());
} else {
retroachievements::load_state(std::ptr::null(), 0);
}
if !ra_state.is_empty() {
retroachievements::load_state(ra_state.as_ptr(), ra_state.len());
} else {
retroachievements::load_state(std::ptr::null(), 0);
}
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
if !rewind {
ui::video::onscreen_message(
&format!(
"Savestate loaded from slot {}",
device.ui.storage.save_state_slot
),
false,
);
} else {
ui::video::onscreen_message(
&format!(
"Failed to load savestate from slot {}",
device.ui.storage.save_state_slot
),
false,
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
} else {
ui::video::onscreen_message(
&format!(
"Could not find savestate in slot {}",
device.ui.storage.save_state_slot
),
false,
);
let (message, length) = if !rewind {
(
format!(
"Failed to load savestate from slot {}",
device.ui.storage.save_state_slot
),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
)
} else if device.netplay.is_none() {
(
"Failed to rewind".to_string(),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
)
} else {
(
"Failed to rollback".to_string(),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
)
};
ui::video::onscreen_message(&message, length);
}
}
+102 -63
View File
@@ -1,3 +1,5 @@
#[cfg(target_os = "android")]
pub mod android;
pub mod audio;
#[cfg(feature = "gui")]
pub mod cheats;
@@ -5,6 +7,7 @@ pub mod config;
#[cfg(feature = "gui")]
pub mod gui;
pub mod input;
pub mod input_profile;
#[cfg(feature = "gui")]
pub mod netplay;
#[cfg(feature = "gui")]
@@ -12,26 +15,48 @@ pub mod retroachievements;
pub mod storage;
pub mod usb;
pub mod video;
#[cfg(feature = "gui")]
#[cfg(all(feature = "gui", not(target_os = "android")))]
pub mod vru;
pub const APP_ID: &str = "io.github.gopher64.gopher64";
pub static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
reqwest::Client::builder()
.user_agent(format!(
"{}/{}",
env!("CARGO_PKG_NAME"),
env!("GIT_DESCRIBE")
))
.build()
.unwrap()
});
#[derive(Default, Clone)]
pub struct Dirs {
pub config_dir: std::path::PathBuf,
pub data_dir: std::path::PathBuf,
pub cache_dir: std::path::PathBuf,
}
#[derive(Default)]
pub struct Audio {
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub gain: f32,
}
unsafe impl Send for Audio {}
unsafe impl Sync for Audio {}
#[derive(Default)]
pub struct Input {
pub joysticks: Vec<sdl3_sys::joystick::SDL_JoystickID>,
pub keyboard_state: *const bool,
pub controllers: [input::Controllers; 4],
}
unsafe impl Send for Input {}
unsafe impl Sync for Input {}
#[derive(Default)]
pub struct Storage {
pub save_type: Vec<storage::SaveTypes>,
pub paths: storage::Paths,
@@ -39,34 +64,40 @@ pub struct Storage {
pub save_state_slot: u32,
}
#[derive(Default)]
pub struct Video {
pub window: *mut sdl3_sys::video::SDL_Window,
pub fullscreen: bool,
pub fps_tx: tokio::sync::mpsc::Sender<bool>,
pub fps_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
pub vis_tx: tokio::sync::mpsc::Sender<bool>,
pub vis_rx: Option<tokio::sync::mpsc::Receiver<bool>>,
pub widescreen: bool,
pub fps_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
pub fps_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
pub vis_tx: Option<tokio::sync::mpsc::UnboundedSender<bool>>,
pub vis_rx: Option<tokio::sync::mpsc::UnboundedReceiver<bool>>,
}
unsafe impl Send for Video {}
unsafe impl Sync for Video {}
#[derive(Default)]
pub struct Usb {
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>,
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
pub usb_tx: Option<tokio::sync::mpsc::UnboundedSender<usb::UsbData>>,
pub cart_rx: Option<tokio::sync::mpsc::UnboundedReceiver<usb::UsbData>>,
}
#[derive(Clone)]
pub struct GameSettings {
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub cheats: rustc_hash::FxHashMap<String, Option<String>>,
pub load_savestate_slot: Option<u32>,
}
#[derive(Default)]
pub struct Ui {
pub dirs: Dirs,
pub config: config::Config,
pub game_id: String,
pub game_hash: String,
pub with_sdl: bool,
pub audio: Audio,
pub input: Input,
pub storage: Storage,
@@ -74,27 +105,34 @@ pub struct Ui {
pub usb: Usb,
}
impl Drop for Ui {
fn drop(&mut self) {
if self.with_sdl {
unsafe {
sdl3_ttf_sys::ttf::TTF_Quit();
sdl3_sys::init::SDL_Quit();
}
}
pub fn sdl_hints() {
unsafe {
let hint = std::ffi::CString::new("1").unwrap();
sdl3_sys::everything::SDL_SetHint(
sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
hint.as_ptr(),
);
sdl3_sys::everything::SDL_SetHint(
sdl3_sys::everything::SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY,
hint.as_ptr(),
);
let app_id = std::ffi::CString::new(APP_ID).unwrap();
sdl3_sys::everything::SDL_SetHint(sdl3_sys::everything::SDL_HINT_APP_ID, app_id.as_ptr());
}
}
pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
unsafe {
if sdl3_sys::init::SDL_WasInit(flag) == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
let err = std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap();
let err = sdl3_sys::error::SDL_GetError();
panic!(
"Could not initialize SDL subsystem: {}, {}",
u32::from(flag),
err
if err.is_null() {
"Unknown error"
} else {
std::ffi::CStr::from_ptr(err).to_str().unwrap()
}
);
}
}
@@ -108,31 +146,44 @@ pub fn ttf_init() {
}
}
pub fn sdl_close() {
unsafe {
sdl3_ttf_sys::ttf::TTF_Quit();
sdl3_sys::init::SDL_Quit();
}
}
pub fn get_dirs() -> Dirs {
let exe_path = std::env::current_exe().unwrap();
let portable_dir = exe_path.parent();
let portable = portable_dir.unwrap().join("portable.txt").exists();
if portable {
Dirs {
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
}
} else {
Dirs {
config_dir: dirs::config_dir().unwrap().join("gopher64"),
data_dir: dirs::data_dir().unwrap().join("gopher64"),
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
#[cfg(target_os = "android")]
return android::get_dirs();
#[cfg(not(target_os = "android"))]
{
let exe_path = std::env::current_exe().unwrap();
let portable_dir = exe_path.parent();
let portable = portable_dir.unwrap().join("portable.txt").exists();
if portable {
Dirs {
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
}
} else {
Dirs {
config_dir: dirs::config_dir().unwrap().join("gopher64"),
data_dir: dirs::data_dir().unwrap().join("gopher64"),
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
}
}
}
}
impl Ui {
fn construct_ui(joysticks: Vec<sdl3_sys::everything::SDL_JoystickID>, with_sdl: bool) -> Ui {
pub fn new() -> Ui {
let dirs = get_dirs();
let (fps_tx, fps_rx) = tokio::sync::mpsc::channel(1000);
let (vis_tx, vis_rx) = tokio::sync::mpsc::channel(1000);
let (fps_tx, fps_rx) = tokio::sync::mpsc::unbounded_channel();
let (vis_tx, vis_rx) = tokio::sync::mpsc::unbounded_channel();
Ui {
input: Input {
controllers: [
@@ -140,25 +191,32 @@ impl Ui {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
input::Controllers {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
input::Controllers {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
input::Controllers {
game_controller: std::ptr::null_mut(),
joystick: std::ptr::null_mut(),
rumble: false,
guid: sdl3_sys::guid::SDL_GUID::default(),
last_key_state: 0,
},
],
keyboard_state: std::ptr::null_mut(),
joysticks,
},
storage: Storage {
save_state_slot: 0,
@@ -195,7 +253,7 @@ impl Ui {
write_pending: false,
},
romsave: storage::RomSave {
data: std::collections::HashMap::new(),
data: rustc_hash::FxHashMap::default(),
write_pending: false,
},
},
@@ -210,9 +268,10 @@ impl Ui {
video: Video {
window: std::ptr::null_mut(),
fullscreen: false,
fps_tx,
widescreen: false,
fps_tx: Some(fps_tx),
fps_rx: Some(fps_rx),
vis_tx,
vis_tx: Some(vis_tx),
vis_rx: Some(vis_rx),
},
usb: Usb {
@@ -220,26 +279,6 @@ impl Ui {
cart_rx: None,
},
dirs,
with_sdl,
}
}
pub fn default() -> Ui {
Self::construct_ui(vec![], false)
}
pub fn new() -> Ui {
sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
let mut num_joysticks = 0;
let joysticks = unsafe { sdl3_sys::joystick::SDL_GetJoysticks(&mut num_joysticks) };
if joysticks.is_null() {
panic!("Could not get joystick list");
}
let mut joystick_vec = vec![];
for i in 0..num_joysticks {
joystick_vec.push(unsafe { *joysticks.add(i as usize) });
}
unsafe { sdl3_sys::stdinc::SDL_free(joysticks as *mut std::ffi::c_void) }
Self::construct_ui(joystick_vec, true)
}
}
+670
View File
@@ -0,0 +1,670 @@
use crate::Args;
use crate::create_runtime;
use crate::run;
use crate::ui;
use clap::Parser;
use jni::objects::{JClass, JObject, JObjectArray, JString};
use jni::refs::Global;
use jni::sys::jint;
use jni::{Env, EnvUnowned, JavaVM, bind_java_type};
use std::os::fd::FromRawFd;
const REQUEST_SELECT_ROM: jint = 1;
const CONFIGURE_INPUT_PROFILE: jint = 2;
const RUN_ROM: jint = 3;
pub static ANDROID_APP: std::sync::Mutex<Option<slint::android::AndroidApp>> =
std::sync::Mutex::new(None);
pub static SELECT_ROM_TX: tokio::sync::Mutex<
Option<tokio::sync::oneshot::Sender<Option<std::path::PathBuf>>>,
> = tokio::sync::Mutex::const_new(None);
pub static WEAK_SLINT_WINDOW: std::sync::Mutex<Option<slint::Weak<ui::gui::AppWindow>>> =
std::sync::Mutex::new(None);
bind_java_type! {
DocumentsContract => "android.provider.DocumentsContract",
fields {
#[allow(non_snake_case)]
static EXTRA_INITIAL_URI: JString,
},
}
bind_java_type! {
AndroidActivity => "android.app.Activity",
type_map = {
AndroidIntent => "android.content.Intent",
AndroidContentResolver => "android.content.ContentResolver",
},
methods {
fn start_activity(intent: AndroidIntent) -> (),
fn start_activity_for_result(intent: AndroidIntent, request_code: jint) -> (),
fn get_content_resolver() -> AndroidContentResolver,
},
fields {
#[allow(non_snake_case)]
static RESULT_OK: jint,
},
}
bind_java_type! {
AndroidIntent => "android.content.Intent",
type_map = {
AndroidUri => "android.net.Uri",
},
fields {
#[allow(non_snake_case)]
static ACTION_VIEW: JString,
#[allow(non_snake_case)]
static ACTION_OPEN_DOCUMENT: JString,
#[allow(non_snake_case)]
static CATEGORY_OPENABLE: JString,
#[allow(non_snake_case)]
static FLAG_ACTIVITY_NEW_TASK: jint,
#[allow(non_snake_case)]
static FLAG_GRANT_READ_URI_PERMISSION: jint,
},
constructors {
fn new(),
fn with_action(action: JString),
},
methods {
fn set_data(uri: AndroidUri) -> AndroidIntent,
fn set_type(r#type: JString) -> AndroidIntent,
fn add_category(category: JString) -> AndroidIntent,
fn get_data() -> AndroidUri,
fn put_extra_string {
sig = (extra: JString, value: JString) -> AndroidIntent,
name = "putExtra",
},
fn put_extra_string_array {
sig = (extra: JString, value: JString[]) -> AndroidIntent,
name = "putExtra",
},
fn get_string_extra(name: JString) -> JString,
fn set_class_name(package_name: JString, class_name: JString) -> AndroidIntent,
},
}
bind_java_type! {
ParcelFileDescriptor => "android.os.ParcelFileDescriptor",
methods {
fn close() -> (),
fn detach_fd() -> jint,
},
}
bind_java_type! {
AndroidContentResolver => "android.content.ContentResolver",
type_map = {
AndroidUri => "android.net.Uri",
ParcelFileDescriptor => "android.os.ParcelFileDescriptor",
},
methods {
fn take_persistable_uri_permission(uri: AndroidUri, flags: jint) -> (),
fn open_file_descriptor(uri: AndroidUri, mode: JString) -> ParcelFileDescriptor,
},
}
bind_java_type! {
AndroidUri => "android.net.Uri",
methods {
static fn parse(uri_string: JString) -> AndroidUri,
fn to_string() -> JString,
static fn decode(path: JString) -> JString,
},
}
bind_java_type! {
pub AndroidInputDevice => "android.view.InputDevice",
fields {
#[allow(non_snake_case)]
static SOURCE_JOYSTICK: jint,
#[allow(non_snake_case)]
static SOURCE_GAMEPAD: jint,
},
methods {
static fn get_device_ids() -> jint[],
static fn get_device(device_id: jint) -> AndroidInputDevice,
fn supports_source(source: jint) -> jboolean,
fn is_virtual() -> jboolean,
fn is_external() -> jboolean,
fn get_vendor_id() -> jint,
fn get_product_id() -> jint,
fn get_name() -> JString,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ControllerInfo {
pub name: String,
/// Stable ID from [`InputDevice.getDescriptor`](https://developer.android.com/reference/android/view/InputDevice#getDescriptor()).
pub descriptor: String,
}
fn argv_to_strings(argc: std::ffi::c_int, argv: *mut *mut std::ffi::c_char) -> Vec<String> {
if argc <= 0 || argv.is_null() {
return Vec::new();
}
unsafe {
(0..argc as usize)
.map(|i| {
std::ffi::CStr::from_ptr(*argv.add(i))
.to_string_lossy()
.into_owned()
})
.collect()
}
}
#[unsafe(no_mangle)]
pub extern "C" fn gopher64_sdl_main(
argc: std::ffi::c_int,
argv: *mut *mut std::ffi::c_char,
) -> std::ffi::c_int {
let (close_tx, handle) = create_runtime();
let _guard = handle.enter();
let raw = argv_to_strings(argc, argv);
let args = Args::try_parse_from(raw).unwrap();
if let Err(err) = run(args, argc as usize) {
close_tx.send(()).unwrap();
eprintln!("Error running game: {err:?}");
return 1;
}
close_tx.send(()).unwrap();
0
}
pub fn spawn_configure_input_profile(
profile_name: slint::SharedString,
dinput: bool,
deadzone: i32,
) {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
if let Err(err) = get_vm(app).attach_current_thread(|env| {
start_configure_input_profile_on_jvm(
env,
app,
profile_name.to_string(),
dinput,
deadzone,
)
}) {
eprintln!("JNI error while starting N64Activity: {err:?}");
}
}
}
fn start_configure_input_profile_on_jvm(
env: &mut Env<'_>,
app: &slint::android::AndroidApp,
profile_name: String,
dinput: bool,
deadzone: i32,
) -> jni::errors::Result<()> {
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
let package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?;
let args_key = JString::from_str(env, "args")?;
let mut args = vec![
JString::from_str(env, "--configure-input-profile")?,
JString::from_str(env, &profile_name)?,
JString::from_str(env, "--deadzone")?,
JString::from_str(env, &deadzone.to_string())?,
];
if dinput {
args.push(JString::from_str(env, "--use-dinput")?);
}
let empty_arg = JString::from_str(env, "")?;
let j_args = JObjectArray::<JString>::new(env, args.len(), empty_arg)?;
for (i, arg) in args.iter().enumerate() {
j_args.set_element(env, i, arg)?;
}
let intent = AndroidIntent::new(env)?
.set_class_name(env, &package_name, &class_name)?
.put_extra_string_array(env, &args_key, &j_args)?;
activity
.as_ref()
.start_activity_for_result(env, &intent, CONFIGURE_INPUT_PROFILE)?;
Ok(())
}
pub fn run_rom(
file_path: std::path::PathBuf,
game_settings: ui::GameSettings,
netplay: Option<ui::gui::NetplayDevice>,
weak: slint::Weak<ui::gui::AppWindow>,
) {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
if let Err(err) = get_vm(app).attach_current_thread(|env| {
start_run_rom_on_jvm(env, app, file_path, game_settings, netplay, weak)
}) {
eprintln!("JNI error while starting N64Activity: {err:?}");
}
}
}
fn start_run_rom_on_jvm(
env: &mut Env<'_>,
app: &slint::android::AndroidApp,
file_path: std::path::PathBuf,
game_settings: ui::GameSettings,
netplay: Option<ui::gui::NetplayDevice>,
weak: slint::Weak<ui::gui::AppWindow>,
) -> jni::errors::Result<()> {
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
let package_name = JString::from_str(env, "io.github.gopher64.gopher64")?;
let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?;
let file_path_key = JString::from_str(env, "file_path")?;
let file_path = file_path.to_str().unwrap();
let cheats_path_key = JString::from_str(env, "cheats_path")?;
let cheats_path = app
.internal_data_path()
.unwrap()
.join("cache")
.join("cheats.json");
let args_key = JString::from_str(env, "args")?;
let mut args = vec![
JString::from_str(env, file_path)?,
JString::from_str(env, "--fullscreen")?,
JString::from_str(env, "--overclock")?,
JString::from_str(env, &game_settings.overclock.to_string())?,
JString::from_str(env, "--disable-expansion-pak")?,
JString::from_str(env, &game_settings.disable_expansion_pak.to_string())?,
];
if let Some(netplay) = netplay {
args.push(JString::from_str(env, "--netplay-server-addr")?);
args.push(JString::from_str(env, &netplay.server_addr)?);
args.push(JString::from_str(env, "--netplay-player-number")?);
args.push(JString::from_str(env, &netplay.player_number.to_string())?);
args.push(JString::from_str(env, "--netplay-number-of-players")?);
args.push(JString::from_str(
env,
&netplay.number_of_players.to_string(),
)?);
args.push(JString::from_str(env, "--netplay-input-delay")?);
args.push(JString::from_str(env, &netplay.input_delay.to_string())?);
args.push(JString::from_str(env, "--cheats")?);
args.push(JString::from_str(env, cheats_path.to_str().unwrap())?);
let f = std::fs::File::create(&cheats_path).unwrap();
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
}
let empty_arg = JString::from_str(env, "")?;
let j_args = JObjectArray::<JString>::new(env, args.len(), empty_arg)?;
for (i, arg) in args.iter().enumerate() {
j_args.set_element(env, i, arg)?;
}
let file_path_string = JString::from_str(env, file_path)?;
let cheats_path_string = JString::from_str(env, cheats_path.to_str().unwrap())?;
let intent = AndroidIntent::new(env)?
.set_class_name(env, &package_name, &class_name)?
.put_extra_string(env, &file_path_key, &file_path_string)?
.put_extra_string(env, &cheats_path_key, &cheats_path_string)?
.put_extra_string_array(env, &args_key, &j_args)?;
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
.unwrap();
activity
.as_ref()
.start_activity_for_result(env, &intent, RUN_ROM)?;
Ok(())
}
fn get_vm(app: &slint::android::AndroidApp) -> JavaVM {
unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }
}
pub fn decode_path(path: &str) -> String {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
match get_vm(app).attach_current_thread(|env| decode_path_on_jvm(env, path)) {
Ok(decoded_path) => decoded_path,
Err(err) => {
eprintln!("JNI error while decoding path: {err:?}");
String::new()
}
}
} else {
eprintln!("Android app not initialized");
String::new()
}
}
fn decode_path_on_jvm(env: &mut Env<'_>, path: &str) -> jni::errors::Result<String> {
let path = JString::from_str(env, path)?;
let decoded_path = AndroidUri::decode(env, &path)?;
Ok(decoded_path.try_to_string(env)?)
}
/// Lists connected gamepads and joysticks using the Android framework.
pub fn list_controllers() -> Vec<ControllerInfo> {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
match get_vm(app).attach_current_thread(list_controllers_on_jvm) {
Ok(controllers) => controllers,
Err(err) => {
eprintln!("JNI error while listing controllers: {err:?}");
Vec::new()
}
}
} else {
eprintln!("Android app not initialized");
Vec::new()
}
}
fn list_controllers_on_jvm(env: &mut Env<'_>) -> jni::errors::Result<Vec<ControllerInfo>> {
let device_ids = AndroidInputDevice::get_device_ids(env)?;
let count = device_ids.len(env)?;
let mut ids = vec![0i32; count];
if count > 0 {
device_ids.get_region(env, 0, &mut ids)?;
}
let mut controllers = Vec::new();
for device_id in ids {
let device = AndroidInputDevice::get_device(env, device_id)?;
if device.is_null() {
continue;
}
let supports_gamepad =
device.supports_source(env, AndroidInputDevice::SOURCE_GAMEPAD(env)?)?;
let supports_joystick =
device.supports_source(env, AndroidInputDevice::SOURCE_JOYSTICK(env)?)?;
if !supports_gamepad && !supports_joystick {
continue;
}
let name = if let Ok(name) = device.get_name(env)
&& let Ok(name) = name.try_to_string(env)
{
name
} else {
ui::input::UNKNOWN_CONTROLLER_NAME.to_string()
};
let descriptor = if let Ok(product_id) = device.get_product_id(env)
&& let Ok(vendor_id) = device.get_vendor_id(env)
{
format!("{}:{}:{}", name, vendor_id, product_id)
} else {
String::new()
};
controllers.push(ControllerInfo { name, descriptor });
}
Ok(controllers)
}
/// Opens a URI in the user's default app via [`Intent::ACTION_VIEW`](https://developer.android.com/reference/android/content/Intent#ACTION_VIEW).
pub fn open_uri(path: &str) {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
let path = path.to_string();
if let Err(err) = get_vm(app).attach_current_thread(|env| open_uri_on_jvm(env, app, &path))
{
eprintln!("JNI error while opening URI: {err:?}");
}
}
}
fn open_uri_on_jvm(
env: &mut Env<'_>,
app: &slint::android::AndroidApp,
path: &str,
) -> jni::errors::Result<()> {
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
let uri_string = JString::from_str(env, path)?;
let uri = AndroidUri::parse(env, &uri_string)?;
let action_view = AndroidIntent::ACTION_VIEW(env)?;
let intent = AndroidIntent::with_action(env, &action_view)?.set_data(env, &uri)?;
activity.as_ref().start_activity(env, &intent)?;
Ok(())
}
pub async fn select_rom(rom_dir: slint::SharedString) -> Option<std::path::PathBuf> {
let (tx, rx) = tokio::sync::oneshot::channel::<Option<std::path::PathBuf>>();
SELECT_ROM_TX.lock().await.replace(tx);
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
if let Err(err) = get_vm(app)
.attach_current_thread(|env| select_rom_on_jvm(env, app, rom_dir.to_string()))
{
eprintln!("JNI error while opening URI: {err:?}");
return None;
}
} else {
eprintln!("Android app not initialized");
return None;
}
rx.await.unwrap_or(None)
}
pub async fn select_gb_rom(_player: i32) -> Option<std::path::PathBuf> {
select_rom(slint::SharedString::new()).await
}
pub async fn select_gb_ram(_player: i32) -> Option<std::path::PathBuf> {
select_rom(slint::SharedString::new()).await
}
fn select_rom_on_jvm(
env: &mut Env<'_>,
app: &slint::android::AndroidApp,
rom_dir: String,
) -> jni::errors::Result<()> {
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
let action = AndroidIntent::ACTION_OPEN_DOCUMENT(env)?;
let category = AndroidIntent::CATEGORY_OPENABLE(env)?;
let mime_type = JString::from_str(env, "*/*")?;
let mut intent = AndroidIntent::with_action(env, &action)?
.set_type(env, &mime_type)?
.add_category(env, &category)?;
if !rom_dir.is_empty() {
let start_dir = JString::from_str(env, rom_dir)?;
let extra_initial_uri = DocumentsContract::EXTRA_INITIAL_URI(env)?;
intent = intent.put_extra_string(env, &extra_initial_uri, &start_dir)?;
}
activity.start_activity_for_result(env, &intent, REQUEST_SELECT_ROM)?;
Ok(())
}
pub fn get_dirs() -> ui::Dirs {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
ui::Dirs {
config_dir: app.internal_data_path().unwrap().join("config"),
data_dir: app.external_data_path().unwrap().join("data"),
cache_dir: app.internal_data_path().unwrap().join("cache"),
}
} else {
panic!("Android app not initialized");
}
}
pub fn get_controller_names() -> Vec<String> {
let mut controllers: Vec<String> = list_controllers().into_iter().map(|c| c.name).collect();
controllers.insert(0, "None".into());
controllers
}
pub fn get_controller_paths() -> Vec<String> {
let mut controller_paths: Vec<String> = list_controllers()
.into_iter()
.map(|c| c.descriptor)
.collect();
controller_paths.insert(0, String::new());
controller_paths
}
pub fn rom_exists(path: &str) -> bool {
get_file_from_uri(&std::path::PathBuf::from(path)).is_some()
}
pub fn get_file_from_uri(path: &std::path::PathBuf) -> Option<std::fs::File> {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
let path = path.to_str().unwrap().into();
match get_vm(app).attach_current_thread(|env| get_file_from_uri_on_jvm(env, app, path)) {
Ok(file) => file,
Err(err) => {
eprintln!("JNI error while opening URI: {err:?}");
return None;
}
}
} else {
eprintln!("Android app not initialized");
None
}
}
fn get_file_from_uri_on_jvm(
env: &mut Env<'_>,
app: &slint::android::AndroidApp,
path: String,
) -> jni::errors::Result<Option<std::fs::File>> {
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
let path = JString::from_str(env, path)?;
let mode = JString::from_str(env, "r")?;
let uri = AndroidUri::parse(env, &path)?;
let content_resolver = activity.as_ref().get_content_resolver(env)?;
let parcel_file_descriptor = content_resolver.open_file_descriptor(env, &uri, &mode);
if let Ok(descriptor) = parcel_file_descriptor
&& !descriptor.is_null()
{
let owned_fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(descriptor.detach_fd(env)?) };
let file = std::fs::File::from(owned_fd);
descriptor.close(env)?;
return Ok(Some(file));
} else {
return Ok(None);
}
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_github_gopher64_gopher64_SlintActivity_nativeOnActivityResult<
'caller,
>(
mut unowned_env: EnvUnowned<'caller>,
_class: JClass<'caller>,
request_code: jint,
result_code: jint,
intent_data: JObject<'caller>,
) {
let outcome = unowned_env.with_env(|env| -> Result<_, jni::errors::Error> {
if result_code == AndroidActivity::RESULT_OK(env)? {
match request_code {
REQUEST_SELECT_ROM => {
if let Some(tx) = SELECT_ROM_TX.blocking_lock().take()
&& !intent_data.is_null()
{
let result_intent =
unsafe { env.as_cast_raw::<AndroidIntent>(&intent_data)? };
let uri = result_intent.as_ref().get_data(env)?;
if uri.is_null() {
return Ok(());
}
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
let activity = unsafe {
env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)?
};
let content_resolver = activity.as_ref().get_content_resolver(env)?;
let take_flags = AndroidIntent::FLAG_GRANT_READ_URI_PERMISSION(env)?;
content_resolver
.take_persistable_uri_permission(env, &uri, take_flags)?;
let path = uri.to_string(env)?;
let _ = tx.send(Some(std::path::PathBuf::from(path.to_string())));
} else {
eprintln!("Android app not initialized");
return Ok(());
}
}
}
CONFIGURE_INPUT_PROFILE => {
if let Ok(weak_app_window) = WEAK_SLINT_WINDOW.lock()
&& let Some(weak_app_window) = weak_app_window.as_ref()
{
let config = ui::config::Config::new();
ui::gui::update_input_profiles(&weak_app_window, &config);
}
}
RUN_ROM => {
let result_intent = unsafe { env.as_cast_raw::<AndroidIntent>(&intent_data)? };
let file_path_key = JString::from_str(env, "file_path")?;
let file_path = result_intent
.as_ref()
.get_string_extra(env, &file_path_key)?
.try_to_string(env)?;
let cheats_path_key = JString::from_str(env, "cheats_path")?;
if let Ok(cheats_path) = result_intent
.as_ref()
.get_string_extra(env, &cheats_path_key)
&& let Ok(cheats_path) = cheats_path.try_to_string(env)
{
let _ = std::fs::remove_file(cheats_path);
}
if let Ok(weak_app_window) = WEAK_SLINT_WINDOW.lock()
&& let Some(weak_app_window) = weak_app_window.as_ref()
{
weak_app_window
.upgrade_in_event_loop(move |handle| {
ui::gui::update_recent_roms(&handle, file_path.into());
handle.set_game_running(false)
})
.unwrap();
}
}
_ => {}
}
}
Ok(())
});
outcome.resolve::<jni::errors::ThrowRuntimeExAndDefault>()
}
+101 -50
View File
@@ -1,19 +1,62 @@
use crate::device;
use crate::netplay;
use crate::ui;
pub fn init(ui: &mut ui::Ui, frequency: u64) {
static ADJUST_LOCKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn init(device: &mut device::Device) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
init_game_audio(ui, frequency);
init_game_audio(device);
}
pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
unsafe extern "C" fn audio_callback(
_userdata: *mut std::ffi::c_void,
audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
additional_amount: i32,
_total_amount: i32,
) {
if additional_amount > 0 {
unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
audio_stream,
vec![0; additional_amount as usize].as_ptr() as *const std::ffi::c_void,
additional_amount,
)
};
adjust_audio_frequency(audio_stream, -0.0005);
}
}
pub fn update_freq(device: &mut device::Device) {
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
freq: device.ai.freq as i32,
channels: 2,
};
unsafe {
sdl3_sys::audio::SDL_SetAudioStreamFormat(
device.ui.audio.audio_stream,
&game_audio_spec,
std::ptr::null(),
);
}
if device.ai.regs[device::ai::AI_STATUS_REG] & device::ai::AI_STATUS_BUSY != 0 {
resume_game_audio(&mut device.ui);
} else {
pause_game_audio(&mut device.ui);
}
}
pub fn init_game_audio(device: &mut device::Device) {
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: device.ai.freq as i32,
channels: 2,
};
ui.audio.audio_stream = unsafe {
device.ui.audio.audio_stream = unsafe {
sdl3_sys::audio::SDL_OpenAudioDeviceStream(
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&game_audio_spec,
@@ -21,14 +64,23 @@ pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
std::ptr::null_mut(),
)
};
if ui.audio.audio_stream.is_null() {
if device.ui.audio.audio_stream.is_null() {
panic!("Could not create audio stream");
}
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream)
sdl3_sys::audio::SDL_SetAudioStreamGain(device.ui.audio.audio_stream, device.ui.audio.gain)
&& sdl3_sys::audio::SDL_SetAudioStreamGetCallback(
device.ui.audio.audio_stream,
Some(audio_callback),
std::ptr::null_mut(),
)
} {
panic!("Could not resume audio stream");
panic!("Could not initialize audio stream");
}
if device.ai.regs[device::ai::AI_STATUS_REG] & device::ai::AI_STATUS_BUSY != 0 {
resume_game_audio(&mut device.ui);
} else {
pause_game_audio(&mut device.ui);
}
}
@@ -36,6 +88,18 @@ pub fn close(ui: &mut ui::Ui) {
close_game_audio(ui);
}
pub fn resume_game_audio(ui: &mut ui::Ui) {
unsafe {
sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream);
}
}
pub fn pause_game_audio(ui: &mut ui::Ui) {
unsafe {
sdl3_sys::audio::SDL_PauseAudioStreamDevice(ui.audio.audio_stream);
}
}
pub fn close_game_audio(ui: &mut ui::Ui) {
unsafe {
if !ui.audio.audio_stream.is_null() {
@@ -55,7 +119,7 @@ pub fn lower_audio_volume(ui: &mut ui::Ui) {
}
ui::video::onscreen_message(
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
false,
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
@@ -69,26 +133,38 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
}
ui::video::onscreen_message(
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
false,
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
);
}
fn adjust_audio_frequency(device: &device::Device, frequency: f32) {
if !device.vi.enable_speed_limiter {
unsafe extern "C" fn lock_callback(
_userdata: *mut std::ffi::c_void,
_id: sdl3_sys::timer::SDL_TimerID,
_interval: u32,
) -> u32 {
ADJUST_LOCKED.store(false, std::sync::atomic::Ordering::Relaxed);
0
}
fn adjust_audio_frequency(audio_stream: *mut sdl3_sys::audio::SDL_AudioStream, frequency: f32) {
if ADJUST_LOCKED.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
ADJUST_LOCKED.store(true, std::sync::atomic::Ordering::Relaxed);
unsafe {
let current_ratio =
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream);
sdl3_sys::timer::SDL_AddTimer(1000, Some(lock_callback), std::ptr::null_mut());
let current_ratio = sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream);
sdl3_sys::everything::SDL_SetAudioStreamFrequencyRatio(
device.ui.audio.audio_stream,
audio_stream,
(current_ratio + frequency).clamp(0.995, 1.005),
);
/*
println!(
"Adjusted audio frequency ratio to {}",
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream)
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(audio_stream)
);
*/
}
@@ -99,6 +175,10 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
return;
}
if netplay::in_rollback(device.netplay.as_ref()) {
return;
}
let mut primary_buffer: Vec<i16> = vec![0; length as usize / 2];
let mut i = 0;
while i < length as usize / 2 {
@@ -116,44 +196,15 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
unsafe { sdl3_sys::audio::SDL_GetAudioStreamQueued(device.ui.audio.audio_stream) } as f64;
let samples_per_frame = device.ai.freq as f64 * device.vi.frame_time * 4.0;
let max_latency = samples_per_frame * 8.0;
if audio_queued < samples_per_frame {
let silence_buffer: Vec<u8> = vec![0; samples_per_frame as usize & !3];
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
device.ui.audio.audio_stream,
silence_buffer.as_ptr() as *const std::ffi::c_void,
silence_buffer.len() as i32,
)
} {
panic!("Could not play audio");
}
/*
println!(
"Audio underrun: queued {} samples, expected at least {} samples",
audio_queued, samples_per_frame
);
*/
adjust_audio_frequency(device, -0.0005);
}
if audio_queued < max_latency {
if !unsafe {
unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
device.ui.audio.audio_stream,
primary_buffer.as_ptr() as *const std::ffi::c_void,
primary_buffer.len() as i32 * 2,
)
} {
panic!("Could not play audio");
}
} else {
/*
println!(
"Audio overrun: queued {} samples, expected at most {} samples",
audio_queued, max_latency
);
*/
adjust_audio_frequency(device, 0.0005);
};
} else if device.speed_limiter.enabled {
adjust_audio_frequency(device.ui.audio.audio_stream, 0.0005);
}
}

Some files were not shown because too many files have changed in this diff Show More