Compare commits

...

265 Commits

Author SHA1 Message Date
Logan McNaughton ca4a20f524 update cargo to v1.1.17 (#798) 2026-04-23 11:02:02 +02:00
Logan McNaughton 3b145909e8 show more detailed version in --version (#797) 2026-04-23 08:57:03 +02:00
Logan McNaughton 6eb3367f1b don't show hardcore switch if it is not enabled (#795) 2026-04-22 09:32:22 +02:00
Logan McNaughton 03f83fe917 fix git version with shallow clones (#794)
* fix git version with shallow clones

* more
2026-04-22 09:08:54 +02:00
Logan McNaughton c4771f91e0 print error to screen (#793)
* print error to screen

* mmore
2026-04-21 22:35:14 +02:00
Danny Kirkham bdd12352e1 Enable accessibility features (#791) 2026-04-21 18:23:10 +02:00
Logan McNaughton 224bd2df1d clean up RA variables (#789)
* clean up RA variables

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-21 12:57:41 +02:00
Logan McNaughton 412f928781 improve function order (#788)
* improve function order

* more
2026-04-20 22:53:42 +02:00
Logan McNaughton 779ef9fe77 update fullscreen note (#787) 2026-04-20 19:27:09 +02:00
Logan McNaughton 048d1d662f Track recent roms (#786)
* track recent roms

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-20 16:53:40 +02:00
Logan McNaughton cf3a27cecf Use switch instead of checkbox (#785) 2026-04-20 12:27:44 +02:00
Logan McNaughton b8c3556df6 cleanup loop (#784) 2026-04-20 10:34:27 +02:00
Logan McNaughton 9e337295ca don't launch game if already running (#783) 2026-04-20 10:13:15 +02:00
Logan McNaughton 8cb57811a6 make rfd optional (#782) 2026-04-20 09:27:29 +02:00
Logan McNaughton 82c02461f8 handle file dropping (#781)
* handle file dropping

* more
2026-04-20 09:11:09 +02:00
Logan McNaughton 26253b5ed7 just create config (#780) 2026-04-20 07:14:05 +02:00
Logan McNaughton f520aeab82 use tokio process (#779) 2026-04-20 07:00:39 +02:00
Logan McNaughton edb6960d4f Make Slint optional (#778)
* make slint optional

* more

* more
2026-04-19 23:15:56 +02:00
Logan McNaughton a488f0d91a fix some typos (#777) 2026-04-19 18:51:01 +02:00
Logan McNaughton 73d895a64a add vsync option (#776) 2026-04-19 18:25:36 +02:00
Logan McNaughton 805bcba249 run VRU dialog in main thread (#774)
* run VRU dialog in main thread

* more
2026-04-19 17:00:50 +02:00
Logan McNaughton 66abd14c3b add small note to netplay join (#773) 2026-04-19 14:09:27 +02:00
Logan McNaughton d856499f59 fix image positioning (#772)
* fix image positioning

* more
2026-04-19 07:57:57 +02:00
Logan McNaughton bc3dd4d39a Hotkey for onscreen FPS (#770)
* fps counter

* more
2026-04-18 21:55:37 +02:00
Logan McNaughton 5a91d09f9e improved transferpak setup (#769)
* improved transferpak setup

* more

* more

* more
2026-04-18 16:48:55 +02:00
Logan McNaughton b09f8bf891 update MACOSX_DEPLOYMENT_TARGET (#768) 2026-04-18 10:08:30 +02:00
Logan McNaughton 8ee10e40ad add provisionprofile (#767)
* add provisionprofile

* more
2026-04-18 08:02:30 +02:00
Logan McNaughton f8a7153fdd enable vsync if required (#765)
* enable vsync if required

* add note

* more

* more
2026-04-17 15:07:15 +02:00
Logan McNaughton a592d0eb84 remove uses of to_owned (#764)
* remove uses of to_owned

* more

* more
2026-04-17 09:59:32 +02:00
Logan McNaughton 28aa8ea280 use tokio::fs (#763) 2026-04-17 09:27:42 +02:00
Logan McNaughton cb87f79710 writeback GB ram files (#762) 2026-04-17 08:56:53 +02:00
Logan McNaughton be117214c3 Update README with notable features of Gopher64 (#761)
Added features list for Gopher64 emulator
2026-04-17 08:48:47 +02:00
Logan McNaughton bb056a77f4 bump version (#760) 2026-04-16 15:56:35 +02:00
Logan McNaughton d4a90155f9 Rust 1.95.0 (#759) 2026-04-16 15:21:01 +02:00
Logan McNaughton e24a9a8126 cleaner fix for MBC30 issue (#758)
* cleaner fix for MBC30 issue

* more
2026-04-16 13:31:17 +02:00
Logan McNaughton 83fab4c638 MBC30 support (#757) 2026-04-16 12:38:45 +02:00
Logan McNaughton 8196b52289 Fix MBC30 support (#756) 2026-04-16 11:56:57 +02:00
Logan McNaughton 8dadbbbf58 update slint to 1.16.0 (#755) 2026-04-16 11:09:09 +02:00
Logan McNaughton 093b39b17d Improve netplay RA guard (#752)
* remove double guard for netplay+RA

* more

* more
2026-04-15 19:00:39 +02:00
Logan McNaughton 7100750e2d show proper error on failed login (#751)
* show proper error on failed login

* more
2026-04-15 16:02:34 +02:00
Logan McNaughton 40e250386e add note about strange char* comparison (#750)
* add note about strange char* comparison

* more
2026-04-15 13:54:27 +02:00
Logan McNaughton d2e47509b2 remove unused param (#749) 2026-04-15 13:39:49 +02:00
Logan McNaughton 451d66beee video code cleanup (#748) 2026-04-15 13:25:04 +02:00
Logan McNaughton 20bad48894 store C strings in vars (#747) 2026-04-15 13:10:15 +02:00
Logan McNaughton b40ec6940f finish up RA support (#745)
* finish up RA support

* more

* capture some corner cases

* fix post data

* more

* more

* fix range

* more

* more

* more

* more

* show in-progress achievements

* more

* more

* more

* more
2026-04-15 12:57:52 +02:00
Logan McNaughton d12f16fe29 minor code cleanup (#746) 2026-04-15 12:36:21 +02:00
dependabot[bot] d061a856d2 Bump rand from 0.10.0 to 0.10.1 in the cargo group across 1 directory (#744)
Bumps the cargo group with 1 update in the / directory: [rand](https://github.com/rust-random/rand).


Updates `rand` from 0.10.0 to 0.10.1
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.0...0.10.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.1
  dependency-type: direct:production
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 07:26:44 +02:00
Logan McNaughton 7725aa2692 RA tweaks (#743)
* prevent crash

* more

* more
2026-04-13 22:38:31 +02:00
Logan McNaughton baa14de3f3 show leaderboard and challenge together (#742) 2026-04-13 21:22:36 +02:00
Logan McNaughton c14b461612 more RA (#741)
* more RA

* more

* more
2026-04-13 20:39:57 +02:00
Logan McNaughton 595089187d leaderboard tracker (#740)
* leaderboard tracker

* more

* more

* more

* more
2026-04-13 13:30:18 +02:00
Logan McNaughton c4b86eb4b1 challenge indicators (#739)
* Add Challenge Indicators

* more

* more

* use c++20

* shorten messages

* progress

* more

* more

* more

* more
2026-04-13 09:16:15 +02:00
Logan McNaughton 4240a99ad5 cache message images (#738) 2026-04-12 19:00:45 +02:00
Logan McNaughton 9d7db93c70 fix font clarity (#737) 2026-04-12 15:21:52 +02:00
Logan McNaughton 9b71d3aa0b print message for failed login (#736) 2026-04-12 12:33:11 +02:00
Logan McNaughton e8898c52fc More RA code (#733)
* implement RC_CLIENT_EVENT_SERVER_ERROR

* handle more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-12 12:10:15 +02:00
Logan McNaughton 4b5c8d03a9 use a const pointer (#732) 2026-04-11 16:35:04 +02:00
Logan McNaughton 2706f8f6a5 use sized savestate functions (#730)
* use sized savestate functions

* fix lint
2026-04-11 08:44:22 +02:00
Logan McNaughton e4391b3089 add some warning messages (#729) 2026-04-10 21:51:06 +02:00
Logan McNaughton 4b54ea279d Build on MacOS 15 (#728) 2026-04-10 20:21:12 +02:00
Logan McNaughton 9f6194d64f better startup message (#727)
* better startup message

* more
2026-04-10 07:56:18 +02:00
Logan McNaughton 8bb3179534 Improve startup messages (#726)
* improve startup messages

* more
2026-04-09 20:05:54 +02:00
Logan McNaughton 35dbd957cc reduce font size a bit (#725)
* reduce font size a bit

* remove duplicate messages
2026-04-09 17:22:26 +02:00
Logan McNaughton 125f79a0e6 show message for RA progress (#724) 2026-04-09 15:15:29 +02:00
Logan McNaughton f865ccfacd pull tags in github action (#723)
* pull tags in github action

* more detailed version

* more
2026-04-09 13:50:48 +02:00
Logan McNaughton 914798e717 more descriptive git naming (#722) 2026-04-09 13:13:56 +02:00
Logan McNaughton 406ebba93d run ra do_idle for VRU window (#721) 2026-04-09 11:31:00 +02:00
Logan McNaughton d8ec09d475 centralize hardcore check (#720) 2026-04-09 11:17:57 +02:00
Logan McNaughton a1da699580 add RA show profile button (#719)
* add RA show profile button

* lint
2026-04-09 09:37:57 +02:00
Logan McNaughton 1226052035 cleanup some cli options (#718) 2026-04-09 09:20:23 +02:00
Logan McNaughton 2917e662df better defaults for window size (#717) 2026-04-08 22:31:28 +02:00
Logan McNaughton 601713e102 RetroAchievements hardcore support (#716)
* enable hardcore mode

* more
2026-04-08 21:57:18 +02:00
Logan McNaughton 6ed53e4e9d Upload signed windows builds (#715)
* upload windows signed artifact

* more

* more

* more

* more
2026-04-08 13:09:32 +02:00
Logan McNaughton 650fd270e1 add messages for RA leaderboards (#714) 2026-04-08 09:54:08 +02:00
Logan McNaughton 54d82713ac dont check for update with flatpak (#713) 2026-04-07 19:42:07 +02:00
Logan McNaughton 11cb242ed2 sign mac bundle (#712)
* sign mac bundle

* xcrun

* use zip

* more
2026-04-07 19:31:04 +02:00
Logan McNaughton e932239a63 RetroAchievements (#710)
* retroachievements

* add rc_api_info

* disable hardcore

* clear password after login attempt

* proper check for hardcore

* more set_hardcore

* change checkbox

* add more stubs

* add more UI

* add button for RA website

* cleanup http callback
2026-04-07 08:59:03 +02:00
Logan McNaughton 5207bd4b27 remove governor crate (#709)
* remove governor crate

* more
2026-04-04 09:27:45 +02:00
Logan McNaughton d378ae187a write savestate in thread (#708) 2026-04-03 16:32:40 +02:00
Logan McNaughton e72ddf73f0 use 4 worker threads (#707) 2026-04-03 16:00:00 +02:00
Logan McNaughton 4bba36f0c3 add libpng to macos build (#706)
* add libpng to macos build

* more

* signing

* more

* more

* more
2026-04-02 14:52:16 +02:00
Logan McNaughton 20b5cf3e4d Improve MacOS opt flags (#705) 2026-04-02 08:19:32 +02:00
Logan McNaughton 973f4bb28d Create MacOS bundle (#701)
* Create MacOS bundle

* more

* more

* more

* Update framework link search path in build.rs

* Update framework link search path in build.rs

* Remove MoltenVK linking from build.rs

Removed MoltenVK framework linking from build script.

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-04-01 20:35:46 +02:00
Logan McNaughton de0db4b46d static link moltenvk (#700)
* static link moltenvk

* more

* more

* static freetype
2026-03-31 20:09:06 +02:00
Logan McNaughton 11d3e52779 use status vs spawn/wait (#699) 2026-03-31 19:12:06 +02:00
Logan McNaughton cef70faf1e move MACOSX_DEPLOYMENT_TARGET 2026-03-31 15:02:53 +02:00
Logan McNaughton 9b0b3e67a4 change dinput arg type 2026-03-31 13:58:56 +02:00
Logan McNaughton ebd22415cd honor setting 2026-03-31 11:40:50 +02:00
Logan McNaughton 5177aaab53 dont pass fullscreen 2026-03-31 11:38:36 +02:00
Logan McNaughton ad5dc8311a remove custom stack size (#698)
* remove custom stack size

* new command

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* config

* more

* more
2026-03-31 10:54:14 +02:00
Logan McNaughton cc538f942f use limiter clock 2026-03-30 09:20:44 +02:00
Logan McNaughton 9528473f11 use Duration for comparing time (#696) 2026-03-29 18:54:35 +02:00
Logan McNaughton 8b770cfd18 Update linux deps based on SDL wiki (#695) 2026-03-28 20:00:44 +01:00
Logan McNaughton 46cb6738a2 Use upstream sse2neon (#694)
* Use upstream sse2neon

* update commit
2026-03-28 19:24:53 +01:00
Logan McNaughton 9b8040873d Update parallel-rdp (#693)
* Update parallel-rdp

* more

* more
2026-03-26 17:02:52 +01:00
Logan McNaughton bff39ab04d Rust 1.94.1 (#692)
* Rust 1.94.1

* more

* more
2026-03-26 16:28:29 +01:00
Logan McNaughton 86ce5795fa fix netplay start if player 1 leaves (#691)
* fix netplay start if player 1 leaves

* more

* more
2026-03-25 10:56:17 +01:00
Logan McNaughton a834a4f9a4 Eliminate macOS Vulkan hint in SDL initialization (#690)
Removed macOS-specific Vulkan library hint from SDL initialization.
2026-03-24 18:06:49 +01:00
Logan McNaughton 1a4de8e911 Don't use caching for releases 2026-03-24 14:43:14 +01:00
Logan McNaughton 0f607e61aa Use MacOS 26 (#689) 2026-03-24 13:38:23 +01:00
Logan McNaughton 6220a566eb Enable Win ARM build (#686)
* Enable Win ARM build

* change ss2neon source

* update sse2neon
2026-03-24 13:29:02 +01:00
Logan McNaughton 5da6a6c3ee clippy fixes (#688)
* clippy fixes

* fix fmt
2026-03-24 11:09:16 +01:00
Logan McNaughton 567dd3a79d Reset hotkey (#682)
* Start soft reset work

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2026-03-21 19:57:27 +01:00
dependabot[bot] 4a0a80b045 Bump rustls-webpki in the cargo group across 1 directory (#685)
Bumps the cargo group with 1 update in the / directory: [rustls-webpki](https://github.com/rustls/webpki).


Updates `rustls-webpki` from 0.103.9 to 0.103.10
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.9...v/0.103.10)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.10
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 10:46:18 +01:00
Logan McNaughton 74b087e77e cargo update 2026-03-20 20:39:02 +01:00
Logan McNaughton faadefe96e format cpp code 2026-03-19 19:17:23 +01:00
Logan McNaughton 72df783d8c Add CLI option to load a save state (#681)
* Add CLI option to load a save state

* more

* more

* more
2026-03-19 14:06:53 +01:00
Logan McNaughton e7dfd02e17 cargo update 2026-03-17 19:38:00 +01:00
Logan McNaughton c6703244b3 better window cleanup 2026-03-17 19:15:19 +01:00
Logan McNaughton 8291a93337 draw every 100ms in input profile creation 2026-03-17 19:09:10 +01:00
Logan McNaughton 3e177ea4be move SDL_HINT_VULKAN_LIBRARY 2026-03-16 14:58:02 +01:00
Logan McNaughton 6de1ef3548 change message in input profile 2026-03-16 14:55:27 +01:00
Logan McNaughton 73a3e6a22a Add hint for MoltenVK (#678) 2026-03-15 10:49:24 +01:00
Logan McNaughton fcceca5976 fix mac build (#676)
* fix mac build

* add freetype

* set deployment target

* test

* test

* more

* test

* more

* more

* more

* more

* test

* put back

* link CoreFoundation

* more

* more

* more
2026-03-12 12:49:59 +01:00
Logan McNaughton acf5e08b97 cargo update 2026-03-10 09:29:49 +01:00
Logan McNaughton 768fcdaf7f bump to v1.1.16 2026-03-05 21:21:47 +01:00
Logan McNaughton 1c6e23b7f2 Update parallel-rdp 2026-03-05 20:42:24 +01:00
Logan McNaughton b4ae7db3dd Update to Rust 1.94.0 (#672) 2026-03-05 20:37:50 +01:00
Logan McNaughton e750b6eed1 upload unzipped artifact (#670) 2026-02-27 19:11:00 +01:00
Logan McNaughton f8b777248a update sdl 2026-02-22 08:12:10 +01:00
Logan McNaughton 51e9b3aa55 cargo update 2026-02-15 21:41:10 +01:00
Logan McNaughton efbeaeab88 rust 1.93.1 2026-02-12 22:49:42 +01:00
Logan McNaughton f24a5e7f05 reapply cheats when loading save state 2026-02-11 16:13:36 +01:00
Logan McNaughton 55170fcca3 use Xoshiro256PlusPlus (#668) 2026-02-09 12:16:10 +01:00
Logan McNaughton b952c45e0b cargo update 2026-02-09 10:44:19 +01:00
Logan McNaughton b13f37baf8 bump to 1.1.15 2026-02-06 09:53:09 +01:00
Logan McNaughton db40ec5034 cargo update (rand) 2026-02-06 09:51:12 +01:00
Logan McNaughton cedb2d5c22 update slint 2026-02-04 18:05:03 +01:00
Logan McNaughton 22d57677cb cargo update 2026-02-03 20:49:46 +01:00
Logan McNaughton f29f497405 bump to 1.1.14 2026-01-29 21:58:40 +01:00
Logan McNaughton 0aab9f71e1 cargo update 2026-01-29 21:58:06 +01:00
Logan McNaughton 7f84eb2aff add missing gamepad button types 2026-01-26 21:44:03 +01:00
Logan McNaughton b460d55714 use SDL_HAPTIC_INFINITY 2026-01-25 13:35:59 +01:00
Logan McNaughton ceaf56c563 bump to 1.1.13 2026-01-22 21:00:26 +01:00
Logan McNaughton 5937922558 fix issue with emux_xdetect 2026-01-22 20:44:17 +01:00
Logan McNaughton e9e71a6e02 Update to rust 1.93.0 (#659) 2026-01-22 20:24:41 +01:00
Logan McNaughton 9775f103e0 flush saves to disk (#658)
* flush saves to disk

* more

* more

* more

* more

* more

* more

* more

* more
2026-01-22 20:09:13 +01:00
Logan McNaughton bf01898c6f remove extra function 2026-01-22 09:22:47 +01:00
Logan McNaughton ce3b12bba9 print message for audio volume 2026-01-21 19:01:50 +01:00
Logan McNaughton 56ff2b260c adjust audio ratios 2026-01-21 12:13:19 +01:00
Logan McNaughton 367c22877b add system for micro adjustments to audio pitch 2026-01-21 11:47:33 +01:00
Logan McNaughton befbacc5b3 remove un-needed var 2026-01-20 15:31:03 +01:00
Logan McNaughton e1a6663bb5 remove needless copy 2026-01-20 13:21:44 +01:00
Logan McNaughton c5185e60c6 tweak ai dma calculation 2026-01-20 13:19:14 +01:00
Logan McNaughton 1aa879d759 tweak audio queue formula 2026-01-20 13:05:32 +01:00
Logan McNaughton bfad71f09f Update parallel-rdp (#656)
* Update parallel-rdp

* update cargo
2026-01-19 13:32:06 +01:00
Logan McNaughton 431b787998 update rfd 2026-01-05 19:27:16 +01:00
Logan McNaughton d48e3dc106 update rfd 2026-01-05 09:54:42 +01:00
Logan McNaughton 8be1ac7740 update SDL crate (#648)
* update SDL crate

* more
2026-01-03 10:07:27 +01:00
Logan McNaughton 56d0b1b2e2 update sse2neon to v1.9.1 2026-01-02 14:41:00 +01:00
Logan McNaughton 7f63bf1fde Update sse2neon to v1.9.0 (#642) 2025-12-22 10:08:09 +01:00
Logan McNaughton 05be960358 cargo update 2025-12-21 16:23:23 +01:00
dependabot[bot] 38a5079acf Bump actions/upload-artifact from 5 to 6 (#641)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  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>
2025-12-15 06:36:17 +01:00
Logan McNaughton 144e31c7fc bump to 1.1.12 2025-12-11 22:08:08 +01:00
Logan McNaughton e7a21171f4 fix windows build (#636) 2025-12-11 21:48:28 +01:00
Logan McNaughton 413da0178e update parallel rdp 2025-12-11 17:57:59 +01:00
Logan McNaughton d8c6669da1 Update to Rust 1.92.0 (#634) 2025-12-11 17:46:50 +01:00
Logan McNaughton c6fcfddf8c some cleanup 2025-12-10 15:12:56 +01:00
Logan McNaughton 0aa5c3e63a cleanup variable 2025-12-10 12:35:22 +01:00
Logan McNaughton 6f8433138d clippy fixes (#632)
* clippy fixes

* fix netplay browser
2025-12-10 10:00:42 +01:00
Logan McNaughton ea5bbfb218 only update controller assignment after user input (#629) 2025-12-06 13:42:10 +01:00
Logan McNaughton 10af598223 cargo update 2025-11-26 16:48:39 +01:00
dependabot[bot] c08511adc5 Bump actions/checkout from 5 to 6 (#622)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [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/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  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>
2025-11-24 04:24:48 +01:00
Logan McNaughton f116617ac3 remove redundant SDL_PumpEvents 2025-11-22 14:31:08 +01:00
Logan McNaughton 1141095b65 some code cleanup 2025-11-22 14:14:22 +01:00
Logan McNaughton 162a6df56d move if statement for better performance 2025-11-22 13:40:26 +01:00
Logan McNaughton 72f47e5aff addressing hack to fix compatibility with latest libdragon (#620) 2025-11-22 10:04:31 +01:00
Logan McNaughton 6d40f5a12d cargo update 2025-11-21 10:44:52 +01:00
Logan McNaughton 64e6d75db1 Update Granite (#618)
* Update Granite

* add file
2025-11-21 10:44:09 +01:00
Logan McNaughton 20761faf8c rust 1.91.1 2025-11-11 12:43:54 +01:00
Adam Steinberg 79eb34d83d unsolicited main.rs refactor (#614)
* main.rs refactor

* remove anyhow

* forgive me clippy, for I have sinned
2025-11-08 21:34:35 +01:00
Logan McNaughton c4057c2f21 bump to 1.1.11 2025-11-05 15:29:56 +01:00
Logan McNaughton 581cc0b9aa cargo update 2025-11-05 15:16:57 +01:00
Logan McNaughton 6f6e21c718 tweak build options 2025-11-05 15:12:12 +01:00
Logan McNaughton 181774a1de pass netplay_id to workflow 2025-11-04 15:38:30 +01:00
Logan McNaughton 002fbbe8a4 set netplay id from environment (#606) 2025-11-04 08:27:32 +01:00
Logan McNaughton ab630cc8cb Use skia backend for gui (#605)
* Use skia backend for gui

* more

* use clang-cl

* more

* more
2025-11-03 23:07:10 +01:00
Logan McNaughton 06f9c56a79 bump to 1.1.10 2025-10-30 22:52:34 +01:00
Logan McNaughton c3f4b11519 update sdl3 2025-10-30 22:47:45 +01:00
Logan McNaughton 921a0d5362 Rust 1.91 (#599) 2025-10-30 20:14:39 +01:00
Logan McNaughton 24fc5b9638 update parallel-rdp 2025-10-30 13:59:20 +01:00
dependabot[bot] 1559856483 Bump signpath/github-action-submit-signing-request from 1.3 to 2.0 (#597)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1.3 to 2.0.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1.3...v2.0)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2.0'
  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>
2025-10-27 08:36:02 +01:00
dependabot[bot] 1deb91fc1e Bump actions/upload-artifact from 4 to 5 (#596)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  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>
2025-10-27 06:01:31 +01:00
Logan McNaughton 726a58f854 cargo update 2025-10-26 13:45:56 +01:00
Logan McNaughton fe0297690f pump events during vi (#593)
* pump events during vi

* add var

* more

* more
2025-10-26 13:42:25 +01:00
Logan McNaughton 847df61526 Add pause between key bindings (#594) 2025-10-26 13:33:40 +01:00
Logan McNaughton 6de8128f5b cargo update 2025-10-12 12:30:57 +02:00
Logan McNaughton 6c09176d12 add .srm for GB saves 2025-10-11 19:32:35 +02:00
Logan McNaughton 2050098735 fix out of bounds access (#590) 2025-10-06 09:02:50 +02:00
Logan McNaughton d3ecc37da6 fix compare interrupt issue (#588) 2025-10-01 10:05:03 +02:00
Logan McNaughton a0237e9f7d bump to 1.1.9 2025-09-28 10:00:37 +02:00
Logan McNaughton d6d608b674 Revert "more audio cleanup"
This reverts commit f3271cba63.
2025-09-28 09:59:02 +02:00
Logan McNaughton f254bec968 bump to 1.1.8 2025-09-27 21:33:21 +02:00
Logan McNaughton bd4aff2b2c cargo update 2025-09-27 21:32:27 +02:00
Logan McNaughton 0bf70a77ea correct load block size 2025-09-26 18:52:27 +02:00
Logan McNaughton e23e7f33c1 Re-add wgpu
I didn't see any evidence of crashing
2025-09-26 07:23:48 +02:00
Logan McNaughton 9d0516545e reorder items 2025-09-25 16:14:48 +02:00
Logan McNaughton e3023a902f further depth buffer tracking tweaks (#583) 2025-09-25 15:59:58 +02:00
Logan McNaughton 431c7c2a8d cargo update 2025-09-25 13:32:23 +02:00
Logan McNaughton cf99539f85 remove wgpu (#576) 2025-09-25 07:36:23 +02:00
Logan McNaughton c2493b829e watch z-buffer reads as well (#581) 2025-09-24 23:00:39 +02:00
Logan McNaughton 2f25de55d7 change depth buffer tracking (#580) 2025-09-24 20:06:39 +02:00
Logan McNaughton 116ffe9845 fix clippy warning 2025-09-22 09:15:41 +02:00
Logan McNaughton f3271cba63 more audio cleanup 2025-09-21 18:36:03 +02:00
Logan McNaughton 9037680cda cleanup audio code (#579) 2025-09-21 16:30:11 +02:00
Logan McNaughton fc41e26a6d expose 8x resolution (#578) 2025-09-21 13:36:13 +02:00
Logan McNaughton d1f28bb7a8 optimize rdp_check_framebuffers (#577) 2025-09-21 10:43:49 +02:00
Logan McNaughton 9db831fdef disable repeat mode on MI_CLR_INIT 2025-09-20 17:21:06 +02:00
Logan McNaughton 004b603e96 support basic mi repeat mode (#575)
* support basic mi repeat mode

* more

* more
2025-09-20 17:09:30 +02:00
Logan McNaughton 8661e2c9b1 always enable lto 2025-09-20 08:37:55 +02:00
Logan McNaughton c712f2a288 bump to 1.1.7 2025-09-19 12:57:05 +02:00
Logan McNaughton 366162e6fe cargo update 2025-09-19 12:56:07 +02:00
Logan McNaughton 2b93fb8aa1 re-add framebuffer tracking (#573) 2025-09-19 11:32:48 +02:00
Logan McNaughton a191701610 update to rust 1.90 (#574)
* update to rust 1.90

* more
2025-09-18 22:46:22 +02:00
Logan McNaughton 0605967c28 modify rsp sync points 2025-09-16 11:39:23 +02:00
Logan McNaughton d6e6a16814 reset device.rdp.last_status_value 2025-09-16 10:02:07 +02:00
Logan McNaughton e374825d5e add release-with-debug profile 2025-09-15 19:23:21 +02:00
Logan McNaughton f6d6746207 use addr_masked 2025-09-15 18:32:25 +02:00
Logan McNaughton 100238f198 optimize some reads/writes (#571) 2025-09-15 18:26:45 +02:00
Logan McNaughton daee53a626 Optimize VU instructions (#570) 2025-09-15 15:50:28 +02:00
Logan McNaughton 018d8ca3e3 fix parallel-rdp build 2025-09-15 12:28:54 +02:00
Logan McNaughton 24d26d864e update parallel-rdp 2025-09-15 12:21:26 +02:00
Logan McNaughton 7b8c549e8e remove rdp sync speed hack (#567) 2025-09-15 09:50:23 +02:00
Logan McNaughton f6c7e3d3c0 use wgpu for gui 2025-09-12 14:16:46 +02:00
Logan McNaughton 374b01dccf cargo update 2025-09-12 13:58:21 +02:00
Logan McNaughton 569b26a067 panic on attempted use of mi repeat mode 2025-09-10 13:16:57 +02:00
Logan McNaughton 380d196ae7 cargo update 2025-09-05 10:54:03 +02:00
Logan McNaughton 4fa1a8ad39 remove redundant get_rooms netplay call (#563)
* remove redundant get_rooms netplay call

* unwrap
2025-09-05 09:32:17 +02:00
Logan McNaughton 8acf3b9de8 bump to 1.1.6 2025-08-24 10:58:58 +02:00
Logan McNaughton b3297f82b2 cargo update 2025-08-24 10:58:01 +02:00
Logan McNaughton 2127705e7b set deadzon max to 99 2025-08-24 07:29:07 +02:00
Logan McNaughton cccb079d63 cleanup deadzone code (#558) 2025-08-24 00:05:03 +02:00
Logan McNaughton c52b680753 use saturating abs for deadzone (#557) 2025-08-23 22:52:04 +02:00
Logan McNaughton 8799650245 add deadzone option (#554)
* add deadzone option

* add deadzone calc

* more
2025-08-23 15:36:03 +02:00
Logan McNaughton 90be2130c1 move cheats back 2025-08-23 09:36:47 +02:00
Logan McNaughton 96af33c434 Split render frame and update screen, move pause loop (#556)
* split render and update screen functions

* move pause loop
2025-08-23 08:54:47 +02:00
Logan McNaughton 0a42152bcb move cheats 2025-08-22 22:42:09 +02:00
Logan McNaughton db335e24d2 render frame before speed limiter (#555) 2025-08-22 07:59:28 +02:00
Logan McNaughton 4857b86897 bump to 1.1.5 (#553) 2025-08-21 15:22:19 +02:00
Logan McNaughton 26022fff72 cargo update 2025-08-21 15:03:50 +02:00
Logan McNaughton 658dede9e9 use dependabot to update rust (#552) 2025-08-20 10:26:26 +02:00
Logan McNaughton 6ad1a31e34 correct joystick hat diagonal input (#551) 2025-08-20 09:15:41 +02:00
Logan McNaughton 4476d476de add frame advance (#549) 2025-08-18 10:42:29 +02:00
Logan McNaughton 8037fb426e UNF loader support (#526) 2025-08-18 08:44:12 +02:00
Logan McNaughton f0381a2cd1 add uppercase file extensions (#548) 2025-08-17 21:01:23 +02:00
Logan McNaughton c8e70e6125 cargo update 2025-08-17 20:56:02 +02:00
dependabot[bot] e9664b1087 Bump actions/checkout from 4 to 5 (#545)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [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/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  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>
2025-08-12 13:02:31 +02:00
Logan McNaughton 3f72700da0 bump to 1.1.4 (#541) 2025-08-07 19:21:54 +02:00
Logan McNaughton 9fcd2835eb Update to rust 1.89.0 (#540) 2025-08-07 13:17:39 +02:00
Logan McNaughton c6767c94a6 move sse2neon to submodule (#539) 2025-08-07 12:17:50 +02:00
Logan McNaughton f1968e3a58 use NearestClamp for image 2025-08-06 23:02:08 +02:00
Logan McNaughton 23fdfff1ac some clippy fixes (#538)
* some clippy fixes

* more
2025-08-06 12:18:40 +02:00
Logan McNaughton fc512d9517 fix netplay for unknown ROMs 2025-08-06 09:12:35 +02:00
dependabot[bot] 3c317460fd Bump signpath/github-action-submit-signing-request from 1.2 to 1.3 (#537)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1.2 to 1.3.
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1.2...v1.3)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '1.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 08:56:09 +02:00
Logan McNaughton b1fcaa5cf4 add minimum size to combobox 2025-08-05 21:55:41 +02:00
Logan McNaughton 98c9cf142d slight improvment to combobox sizes 2025-08-05 21:40:31 +02:00
Logan McNaughton 1eb1503981 Onscreen drawing (#536)
* onscreen messages

* more

* more

* more

* more

* use sdl_ttf

* more

* more

* more

* more

* test

* mroe

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-08-05 15:59:49 +02:00
Logan McNaughton 5aa9a65e7b add support for save state slots (#534)
* add support for save state slots

* add log
2025-08-04 14:59:50 +02:00
Logan McNaughton 71f1d98132 bump to 1.1.3 (#533) 2025-08-03 13:30:27 +02:00
Logan McNaughton 0037c0f344 Don't wait for signpath completion 2025-08-03 11:56:12 +02:00
Logan McNaughton 752e804101 refreshing session message 2025-08-03 09:38:23 +02:00
Logan McNaughton 73354246f3 netplay dispatcher (#532)
* netplay dispatcher

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-08-03 09:25:57 +02:00
Logan McNaughton a2d23c2f41 Fix crash in netplay windows (#530)
* Fix crash in netplay windows

* more
2025-07-29 23:02:07 +02:00
dependabot[bot] 6e652c7dbd Bump signpath/github-action-submit-signing-request from 1.1 to 1.2 (#529)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1.1 to 1.2.
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1.1...v1.2)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '1.2'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 08:50:59 +02:00
Logan McNaughton 20749cf036 bump to 1.1.2 (#528) 2025-07-25 21:45:52 +02:00
93 changed files with 7759 additions and 13564 deletions
+10 -3
View File
@@ -1,16 +1,23 @@
[target.'cfg(target_os = "linux")']
[target.'cfg(all(target_arch = "aarch64", target_os = "linux"))']
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.'cfg(target_os = "windows")']
[target.'cfg(any(target_os = "windows", target_os = "macos"))']
linker = "rust-lld"
[target.'cfg(target_os = "windows")']
rustflags = ["-C", "link-arg=/STACK:8388608"]
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-C", "target-cpu=x86-64-v3"]
[target.'cfg(target_arch = "aarch64")']
[target.'cfg(all(target_arch = "aarch64", target_os = "macos"))']
rustflags = ["-C", "target-cpu=apple-m1"]
[target.'cfg(all(target_arch = "aarch64", not(target_os = "macos")))']
rustflags = ["-C", "target-cpu=cortex-a76"]
[env]
CC = "clang"
CXX = "clang++"
AR = "llvm-ar"
SDKROOT = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
+6 -1
View File
@@ -3,7 +3,12 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
- package-ecosystem: "rust-toolchain"
directory: "/"
schedule:
interval: "weekly"
# - package-ecosystem: "cargo"
# directory: "/"
# schedule:
+97 -31
View File
@@ -4,14 +4,15 @@ permissions:
on:
push:
branches: ["main"]
branches: [ "main" ]
tags:
- "v*"
pull_request:
branches: ["main"]
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
RA_HARDCORE: ${{ github.ref_type == 'tag' && 'true' || 'false' }}
jobs:
build-linux:
@@ -29,42 +30,52 @@ jobs:
arch: linux-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut -d':' -f2 | xargs)" >> $GITHUB_ENV
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
uses: awalsh128/cache-apt-pkgs-action@v1
with:
packages: build-essential git make \
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev 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 fcitx-libs-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
packages: build-essential git make pkg-config cmake ninja-build
gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev
libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev
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
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-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-${{ matrix.llvm }}.tar.xz
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 }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
env:
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
- name: Upload file
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}
archive: false
build-windows:
runs-on: ${{ matrix.os }}
@@ -76,18 +87,20 @@ jobs:
target: x86_64-pc-windows-msvc
arch: windows-x86_64
llvm: win64
#- os: windows-11-arm
# target: aarch64-pc-windows-msvc
# arch: windows-aarch64
# llvm: woa64
- os: windows-11-arm
target: aarch64-pc-windows-msvc
arch: windows-aarch64
llvm: woa64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
run: |
$llvm_version = rustc --version --verbose | Select-String "LLVM version" | ForEach-Object { $_.Line.Split(":")[1].Trim() }
@@ -96,21 +109,29 @@ jobs:
uses: KyleMayes/install-llvm-action@v2
with:
version: ${{ env.LLVM_VERSION }}
force-url: https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-${{ matrix.llvm }}.exe
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 }}
mkdir output
cp target/${{ matrix.target }}/release/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@v4
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}.exe
archive: ${{ github.ref_type == 'tag' && true || false }}
- name: SignPath
if: ${{ github.ref_type == 'tag' }}
uses: signpath/github-action-submit-signing-request@v1.1
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
@@ -119,6 +140,16 @@ jobs:
signing-policy-slug: "release-signing"
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: output-signed
- name: Upload signed artifact
if: ${{ github.ref_type == 'tag' }}
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output-signed/gopher64-${{ matrix.arch }}.exe
archive: false
overwrite: true
build-macos:
runs-on: macos-15
@@ -126,27 +157,62 @@ jobs:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
arch: mac-x86_64
- target: aarch64-apple-darwin
arch: mac-aarch64
arch: macos-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
rustflags: ""
cache: ${{ github.ref_type != 'tag' }}
- name: Set LLVM_VERSION
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-${{
env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-macOS-${{
matrix.llvm }}.tar.xz
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
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
env:
AR: ar
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
MACOSX_DEPLOYMENT_TARGET: "15.7"
- 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
- name: Import Codesign Certs
if: ${{ github.ref_type == 'tag' }}
uses: apple-actions/import-codesign-certs@v6
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
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
- name: Upload file
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}
path: target/${{ matrix.target }}/release/bundle/osx
+16 -12
View File
@@ -2,9 +2,9 @@ name: Rust-lint
on:
push:
branches: ["main"]
branches: [ "main" ]
pull_request:
branches: ["main"]
branches: [ "main" ]
permissions:
contents: read
@@ -16,33 +16,37 @@ jobs:
lint-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy, rustfmt
- name: Set LLVM_VERSION
run: echo "LLVM_VERSION=$(rustc --version --verbose | grep "LLVM version" | cut -d':' -f2 | xargs)" >> $GITHUB_ENV
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
uses: awalsh128/cache-apt-pkgs-action@v1
with:
packages: build-essential git make \
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev 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 fcitx-libs-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
packages: build-essential git make pkg-config cmake ninja-build
gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev
libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev
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
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-${{ env.LLVM_VERSION }}/LLVM-${{ env.LLVM_VERSION }}-Linux-X64.tar.xz
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 clippy -- -Dwarnings
cargo clippy --no-default-features -- -Dwarnings
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
checks: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: rustsec/audit-check@v2
+9 -2
View File
@@ -1,4 +1,11 @@
[submodule "parallel-rdp/parallel-rdp-standalone"]
path = parallel-rdp/parallel-rdp-standalone
url = https://github.com/gopher64/parallel-rdp-standalone.git
branch = gopher64
url = https://github.com/Themaister/parallel-rdp-standalone.git
branch = master
[submodule "src/compat/sse2neon"]
path = src/compat/sse2neon
url = https://github.com/DLTcollab/sse2neon.git
branch = master
[submodule "retroachievements/rcheevos"]
path = retroachievements/rcheevos
url = https://github.com/RetroAchievements/rcheevos.git
Generated
+2504 -1543
View File
File diff suppressed because it is too large Load Diff
+32 -16
View File
@@ -1,47 +1,63 @@
[package]
name = "gopher64"
version = "1.1.1"
version = "1.1.17"
edition = "2024"
rust-version = "1.88"
rust-version = "1.95.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["gui"]
gui = ["dep:rfd", "dep:slint", "dep:slint-build"]
[dependencies]
dirs = "6.0"
zip = "4.3"
governor = "0.10"
sevenz-rust2 = { version = "0.17", default-features = false }
zip = "8.0"
sevenz-rust2 = "0.20"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg"] }
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.10"
ab_glyph = "0.2"
sdl3-sys = { version = "0.5", features = ["build-from-source-static"] }
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros"] }
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"] }
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
regex = "1.11"
tokio-tungstenite = "0.27"
tokio-tungstenite = "0.29"
futures = "0.3"
socket2 = { version = "0.6", features = ["all"] }
rand_chacha = "0.9"
rand = "0.10"
fatfs = "0.3"
semver = "1.0"
postcard = { version = "1.1", features = ["use-std"] }
clap = { version = "4.5", features = ["derive"] }
reqwest = { version = "0.12", default-features = false, features = ["http2","rustls-tls","charset","json"] }
reqwest = { version = "0.13", features = ["json", "query"] }
[build-dependencies]
winresource = "0.1"
bindgen = "0.72"
slint-build = "1.12"
slint-build = { version = "1.12", optional = true }
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
panic = "abort"
lto = "thin"
strip = true
[profile.release-with-debug]
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"
+19 -5
View File
@@ -1,8 +1,20 @@
# gopher64
Gopher64 is a cross-platform N64 emulator. Some notable features:
* Netplay
* Homebrew support
* Upscaling
* CRT shader
* Emulate CPU overclocking
* Cheats
* Savestates
* RetroAchievements
## download
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
MacOS: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-macos-aarch64.zip
Linux: https://flathub.org/apps/io.github.gopher64.gopher64
## wiki
@@ -11,7 +23,7 @@ https://github.com/gopher64/gopher64/wiki
## discord
https://discord.gg/9RGXq8W8JQ
[Discord invite link](https://discord.gg/9RGXq8W8JQ)
## controls
@@ -19,7 +31,7 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher
## netplay
Gopher64 supports netplay (online play with others). It has a few public netplay servers. If you are interested in running a public netplay server, please let me know (open an issue or discussion or contact me on Discord). You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN.
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.
## portable mode
@@ -35,8 +47,8 @@ flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
## building and usage
1. Linux only: install the SDL3 dependencies: https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
2. Install rust: https://www.rust-lang.org/tools/install
1. Linux only: [install the SDL3 dependencies](https://wiki.libsdl.org/SDL3/README-linux#build-dependencies)
2. [Install rust](https://www.rust-lang.org/tools/install)
3. `git clone --recursive https://github.com/gopher64/gopher64.git`
4. `cd gopher64`
5. `cargo build --release`
@@ -48,10 +60,12 @@ I am very open to contributions! Please contact me via a GitHub issue or Discord
## license
Gopher64 is licensed under the GPLv3 license. Many portions of gopher64 have been adapted from mupen64plus and/or ares. The license for mupen64plus can be found here: https://github.com/mupen64plus/mupen64plus-core/blob/master/LICENSES. The license for ares can be found here: https://github.com/ares-emulator/ares/blob/master/LICENSE.
Gopher64 is licensed under the GPLv3 license. Many portions of gopher64 have been adapted from mupen64plus and/or ares. The license for mupen64plus can be found [here](https://github.com/mupen64plus/mupen64plus-core/blob/master/LICENSES). The license for ares can be found [here](https://github.com/ares-emulator/ares/blob/master/LICENSE).
## privacy and code signing policy
Free code signing for the Windows release is provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org).
During online netplay sessions, the server logs your IP address and basic session information (game title and session name) for operational purposes. No additional personal data is collected or stored.
If you enable the RetroAchievements feature, some data is sent to their systems. Please see their terms [here](https://retroachievements.org/terms).
+141 -20
View File
@@ -1,9 +1,13 @@
fn main() {
println!("cargo::rerun-if-changed=parallel-rdp");
println!("cargo::rerun-if-changed=retroachievements");
println!("cargo::rerun-if-changed=src/compat");
let slint_config = slint_build::CompilerConfiguration::new().with_style("cosmic".into());
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
#[cfg(feature = "gui")]
{
let slint_config = slint_build::CompilerConfiguration::new();
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
}
let mut simd_build = cc::Build::new();
let mut volk_build = cc::Build::new();
@@ -14,7 +18,7 @@ fn main() {
let mut rdp_build = cc::Build::new();
rdp_build
.cpp(true)
.std("c++23")
.std("c++20")
.flag("-Wno-unused-parameter")
.flag("-Wno-missing-field-initializers")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/command_ring.cpp")
@@ -36,7 +40,6 @@ fn main() {
.file("parallel-rdp/parallel-rdp-standalone/vulkan/image.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/indirect_layout.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/memory_allocator.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/pipeline_cache.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/pipeline_event.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/query_pool.cpp")
.file("parallel-rdp/parallel-rdp-standalone/vulkan/render_pass.cpp")
@@ -62,16 +65,55 @@ fn main() {
.include("parallel-rdp/parallel-rdp-standalone/vulkan-headers/include")
.include("parallel-rdp/parallel-rdp-standalone/util")
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").to_owned().unwrap())
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").unwrap()).join("include"),
)
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_TTF_OUT_DIR").unwrap())
.join("include"),
);
let mut retroachievements_build = cc::Build::new();
retroachievements_build
.flag("-Wno-unused-parameter")
.include("retroachievements/rcheevos/include")
.flag("-DRC_CLIENT_SUPPORTS_HASH")
.file("retroachievements/rcheevos/src/rc_client.c")
.file("retroachievements/rcheevos/src/rc_compat.c")
.file("retroachievements/rcheevos/src/rc_util.c")
.file("retroachievements/rcheevos/src/rcheevos/alloc.c")
.file("retroachievements/rcheevos/src/rcheevos/condition.c")
.file("retroachievements/rcheevos/src/rcheevos/condset.c")
.file("retroachievements/rcheevos/src/rcheevos/consoleinfo.c")
.file("retroachievements/rcheevos/src/rcheevos/format.c")
.file("retroachievements/rcheevos/src/rcheevos/lboard.c")
.file("retroachievements/rcheevos/src/rcheevos/memref.c")
.file("retroachievements/rcheevos/src/rcheevos/operand.c")
.file("retroachievements/rcheevos/src/rcheevos/richpresence.c")
.file("retroachievements/rcheevos/src/rcheevos/runtime.c")
.file("retroachievements/rcheevos/src/rcheevos/runtime_progress.c")
.file("retroachievements/rcheevos/src/rcheevos/trigger.c")
.file("retroachievements/rcheevos/src/rcheevos/value.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_common.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_info.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_runtime.c")
.file("retroachievements/rcheevos/src/rapi/rc_api_user.c")
.file("retroachievements/rcheevos/src/rhash/aes.c")
.file("retroachievements/rcheevos/src/rhash/cdreader.c")
.file("retroachievements/rcheevos/src/rhash/md5.c")
.file("retroachievements/rcheevos/src/rhash/hash.c")
.file("retroachievements/rcheevos/src/rhash/hash_disc.c")
.file("retroachievements/rcheevos/src/rhash/hash_encrypted.c")
.file("retroachievements/rcheevos/src/rhash/hash_rom.c")
.file("retroachievements/rcheevos/src/rhash/hash_zip.c")
.file("retroachievements/retroachievements.c");
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let profile = std::env::var("PROFILE").unwrap();
let opt_flag = if arch == "x86_64" {
"-march=x86-64-v3"
} else if arch == "aarch64" {
} else if arch == "aarch64" && os == "macos" {
"-march=armv8.4-a"
} else if arch == "aarch64" && os != "macos" {
"-march=armv8.2-a"
} else {
panic!("unknown arch")
@@ -80,6 +122,7 @@ fn main() {
volk_build.flag(opt_flag);
rdp_build.flag(opt_flag);
simd_build.flag(opt_flag);
retroachievements_build.flag(opt_flag);
if os == "windows" {
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
@@ -89,30 +132,85 @@ fn main() {
.set_icon("data/icon/icon.ico")
.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()
.unwrap();
let runtime_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
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");
}
if profile == "release" {
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
simd_build.flag("-flto=thin");
}
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
simd_build.flag("-flto=thin");
retroachievements_build.flag("-flto=thin");
volk_build.compile("volk");
rdp_build.compile("parallel-rdp");
retroachievements_build.compile("retroachievements");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
let mut retroachievements_builder = bindgen::Builder::default()
.header("retroachievements/retroachievements.h")
.allowlist_function("ra_init_client")
.allowlist_function("ra_welcome")
.allowlist_function("ra_shutdown_client")
.allowlist_function("ra_get_hardcore")
.allowlist_function("ra_load_game")
.allowlist_function("ra_unload_game")
.allowlist_function("ra_set_dmem")
.allowlist_function("ra_do_frame")
.allowlist_function("ra_do_idle")
.allowlist_function("ra_http_callback")
.allowlist_function("ra_login_user")
.allowlist_function("ra_login_token_user")
.allowlist_function("ra_state_size")
.allowlist_function("ra_save_state")
.allowlist_function("ra_load_state");
if cfg!(feature = "gui") {
retroachievements_builder = retroachievements_builder
.allowlist_function("ra_logout_user")
.allowlist_function("ra_get_username")
.allowlist_function("ra_get_token");
}
let retroachievements_bindings = retroachievements_builder
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
retroachievements_bindings
.write_to_file(out_path.join("retroachievements_bindings.rs"))
.expect("Couldn't write bindings!");
let parallel_bindings = bindgen::Builder::default()
.header("parallel-rdp/interface.hpp")
.allowlist_function("rdp_init")
.allowlist_function("rdp_close")
.allowlist_function("rdp_set_vi_register")
.allowlist_function("rdp_update_screen")
.allowlist_function("rdp_render_frame")
.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_set_fps")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
@@ -123,7 +221,7 @@ fn main() {
if arch == "aarch64" {
let simd_bindings = bindgen::Builder::default()
.header("src/compat/sse2neon/v1.8.0/sse2neon.h")
.header("src/compat/sse2neon/sse2neon.h")
.allowlist_function("_mm_setzero_si128")
.allowlist_function("_mm_set_epi8")
.allowlist_function("_mm_movemask_epi8")
@@ -157,6 +255,7 @@ fn main() {
.allowlist_function("_mm_cmplt_epi16")
.allowlist_function("_mm_subs_epu16")
.allowlist_function("_mm_set1_epi32")
.allowlist_function("_mm_set1_epi16")
.blocklist_type("__m128i")
.blocklist_type("int64x2_t")
.wrap_static_fns(true)
@@ -178,13 +277,35 @@ fn main() {
.compile("simd");
}
let git_output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
let mut git_output = std::process::Command::new("git");
git_output.args(["describe", "--dirty"]);
let git_hash = String::from_utf8(git_output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={git_hash}");
let git_describe = if let Ok(git_output) = git_output.output()
&& git_output.status.success()
{
String::from_utf8(git_output.stdout).unwrap()
} else if let Ok(git_output) = git_output.args(["--always"]).output()
&& git_output.status.success()
{
format!(
"v{}-{}",
env!("CARGO_PKG_VERSION"),
String::from_utf8(git_output.stdout).unwrap()
)
} else {
panic!("Failed to get git describe");
};
println!("cargo:rustc-env=GIT_DESCRIBE={git_describe}");
println!("cargo:rustc-env=N64_STACK_SIZE={}", 8 * 1024 * 1024);
println!("cargo:rerun-if-env-changed=NETPLAY_ID");
let netplay_id = std::env::var("NETPLAY_ID").unwrap_or("gopher64".to_string());
println!("cargo:rustc-env=NETPLAY_ID={netplay_id}");
println!("cargo:rerun-if-env-changed=RA_HARDCORE");
println!("cargo:rustc-check-cfg=cfg(ra_hardcore_enabled)");
if let Ok(ra_hardcore) = std::env::var("RA_HARDCORE")
&& ra_hardcore.to_lowercase() == "true"
{
println!("cargo:rustc-cfg=ra_hardcore_enabled");
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
<?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.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
+18
View File
@@ -0,0 +1,18 @@
<?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.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+841 -496
View File
File diff suppressed because it is too large Load Diff
+53 -38
View File
@@ -4,48 +4,63 @@
#include <cstdint>
#include <stddef.h>
extern "C"
{
extern "C" {
#endif
typedef struct
{
uint8_t *RDRAM;
uint8_t *DMEM;
uint32_t RDRAM_SIZE;
uint32_t *DPC_CURRENT_REG;
uint32_t *DPC_START_REG;
uint32_t *DPC_END_REG;
uint32_t *DPC_STATUS_REG;
bool PAL;
bool widescreen;
bool fullscreen;
bool integer_scaling;
uint32_t upscale;
bool crt;
} GFX_INFO;
typedef struct {
uint8_t *RDRAM;
uint8_t *DMEM;
uint32_t RDRAM_SIZE;
uint32_t *DPC_CURRENT_REG;
uint32_t *DPC_START_REG;
uint32_t *DPC_END_REG;
uint32_t *DPC_STATUS_REG;
bool PAL;
bool widescreen;
bool fullscreen;
bool vsync;
bool integer_scaling;
uint32_t upscale;
bool crt;
} GFX_INFO;
typedef struct
{
bool emu_running;
bool save_state;
bool load_state;
bool enable_speedlimiter;
bool lower_volume;
bool raise_volume;
bool paused;
} CALL_BACK;
typedef struct {
bool emu_running;
bool save_state;
bool load_state;
bool reset_game;
bool enable_speedlimiter;
bool lower_volume;
bool raise_volume;
bool paused;
bool frame_advance;
uint32_t save_state_slot;
} CALL_BACK;
void rdp_init(void *_window, GFX_INFO _gfx_info);
void rdp_close();
void rdp_set_vi_register(uint32_t reg, uint32_t value);
void rdp_update_screen();
CALL_BACK rdp_check_callback();
uint64_t rdp_process_commands();
void rdp_new_processor(GFX_INFO _gfx_info);
size_t rdp_state_size();
void rdp_save_state(uint8_t *state);
void rdp_load_state(const uint8_t *state);
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font,
size_t font_size, uint32_t save_state_slot);
void rdp_close();
void rdp_set_vi_register(uint32_t reg, uint32_t value);
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_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_set_fps(uint32_t fps, uint32_t vis);
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,
const char *progress);
void achievement_progress_remove();
void leaderboard_tracker_add(uint32_t id, const char *title,
const char *display);
void leaderboard_tracker_remove(uint32_t id);
#ifdef __cplusplus
}
+36 -54
View File
@@ -1,73 +1,55 @@
#include "wsi_platform.hpp"
#include <SDL3/SDL_vulkan.h>
VkSurfaceKHR SDL_WSIPlatform::create_surface(VkInstance instance, VkPhysicalDevice gpu)
{
VkSurfaceKHR surface = nullptr;
bool result = SDL_Vulkan_CreateSurface(window, instance, NULL, &surface);
if (result != true)
{
printf("Error creating surface\n");
}
return surface;
VkSurfaceKHR SDL_WSIPlatform::create_surface(VkInstance instance,
VkPhysicalDevice gpu) {
VkSurfaceKHR surface = nullptr;
bool result = SDL_Vulkan_CreateSurface(window, instance, NULL, &surface);
if (result != true) {
printf("Error creating surface\n");
}
return surface;
}
void SDL_WSIPlatform::destroy_surface(VkInstance instance, VkSurfaceKHR surface)
{
SDL_Vulkan_DestroySurface(instance, surface, NULL);
void SDL_WSIPlatform::destroy_surface(VkInstance instance,
VkSurfaceKHR surface) {
SDL_Vulkan_DestroySurface(instance, surface, NULL);
}
std::vector<const char *> SDL_WSIPlatform::get_instance_extensions()
{
std::vector<const char *> SDL_WSIPlatform::get_instance_extensions() {
unsigned int extensionCount = 0;
char const *const *extensions = SDL_Vulkan_GetInstanceExtensions(&extensionCount);
if (extensions == NULL)
{
printf("Error getting instance extensions\n");
}
unsigned int extensionCount = 0;
char const *const *extensions =
SDL_Vulkan_GetInstanceExtensions(&extensionCount);
if (extensions == NULL) {
printf("Error getting instance extensions\n");
}
std::vector<const char *> extensionNames;
for (unsigned int i = 0; i < extensionCount; ++i)
{
extensionNames.push_back(extensions[i]);
}
return extensionNames;
std::vector<const char *> extensionNames;
for (unsigned int i = 0; i < extensionCount; ++i) {
extensionNames.push_back(extensions[i]);
}
return extensionNames;
}
uint32_t SDL_WSIPlatform::get_surface_width()
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
return w;
uint32_t SDL_WSIPlatform::get_surface_width() {
int w, h;
SDL_GetWindowSize(window, &w, &h);
return w;
}
uint32_t SDL_WSIPlatform::get_surface_height()
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
return h;
uint32_t SDL_WSIPlatform::get_surface_height() {
int w, h;
SDL_GetWindowSize(window, &w, &h);
return h;
}
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi)
{
return true;
}
bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi) { return true; }
void SDL_WSIPlatform::poll_input()
{
}
void SDL_WSIPlatform::poll_input() {}
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler)
{
}
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler) {}
void SDL_WSIPlatform::set_window(SDL_Window *_window)
{
window = _window;
}
void SDL_WSIPlatform::set_window(SDL_Window *_window) { window = _window; }
void SDL_WSIPlatform::do_resize()
{
resize = true;
}
void SDL_WSIPlatform::do_resize() { resize = true; }
+13 -13
View File
@@ -3,20 +3,20 @@
#include "wsi.hpp"
#include <SDL3/SDL.h>
class SDL_WSIPlatform : public Vulkan::WSIPlatform
{
class SDL_WSIPlatform : public Vulkan::WSIPlatform {
public:
VkSurfaceKHR create_surface(VkInstance instance, VkPhysicalDevice gpu) override;
void destroy_surface(VkInstance instance, VkSurfaceKHR surface) override;
std::vector<const char *> get_instance_extensions() override;
uint32_t get_surface_width() override;
uint32_t get_surface_height() override;
bool alive(Vulkan::WSI &wsi) override;
void poll_input() override;
void poll_input_async(Granite::InputTrackerHandler *handler) override;
void set_window(SDL_Window *_window);
void do_resize();
VkSurfaceKHR create_surface(VkInstance instance,
VkPhysicalDevice gpu) override;
void destroy_surface(VkInstance instance, VkSurfaceKHR surface) override;
std::vector<const char *> get_instance_extensions() override;
uint32_t get_surface_width() override;
uint32_t get_surface_height() override;
bool alive(Vulkan::WSI &wsi) override;
void poll_input() override;
void poll_input_async(Granite::InputTrackerHandler *handler) override;
void set_window(SDL_Window *_window);
void do_resize();
private:
SDL_Window *window;
SDL_Window *window;
};
+399
View File
@@ -0,0 +1,399 @@
#include "retroachievements.h"
#include "../parallel-rdp/interface.hpp"
#include <rc_client.h>
#include <rc_consoles.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void rust_server_call(const char *url, const char *post_data,
const char *content_type,
rc_client_server_callback_t callback,
void *callback_data);
void store_retroachievements_credentials(const char *username,
const char *token, void *userdata);
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 bool g_challenge = false;
static bool g_leaderboard = false;
static rc_client_leaderboard_list_t *g_leaderboard_list = NULL;
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)
memset(buffer, 0, num_bytes);
else
memcpy(buffer, &g_dmem[address], num_bytes);
return num_bytes;
}
static void server_call(const rc_api_request_t *request,
rc_client_server_callback_t callback,
void *callback_data, rc_client_t *client) {
rust_server_call(request->url, request->post_data, request->content_type,
callback, callback_data);
}
void ra_http_callback(const char *content, size_t content_size, int status_code,
void *callback, void *callback_data) {
// Prepare a data object to pass the HTTP response to the callback
rc_api_server_response_t server_response;
memset(&server_response, 0, sizeof(server_response));
server_response.body = content;
server_response.body_length = content_size;
server_response.http_status_code = status_code;
// handle non-http errors (socket timeout, no internet available, etc)
if (status_code == 0) {
// Let rc_client know the error was not catastrophic and could be retried.
// It may decide to retry or just immediately pass the error to the
// callback. To prevent possible retries, use
// RC_API_SERVER_RESPONSE_CLIENT_ERROR.
server_response.http_status_code =
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR;
}
((rc_client_server_callback_t)callback)(&server_response, callback_data);
}
static void log_message(const char *message, const rc_client_t *client) {
printf("RetroAchievements: %s\n", message);
}
static void login_callback(int result, const char *error_message,
rc_client_t *client, void *userdata) {
// If not successful, just report the error and bail.
if (result != RC_OK) {
if (client && !rc_client_get_userdata(client))
rc_client_set_userdata(client, strdup(error_message));
store_retroachievements_credentials(NULL, NULL, userdata);
return;
}
// Login was successful. Capture the token for future logins so we don't have
// to store the password anywhere.
const rc_client_user_t *user = rc_client_get_user_info(client);
store_retroachievements_credentials(user->username, user->token, userdata);
}
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); }
void ra_login_user(const char *username, const char *password, void *userdata) {
// This will generate an HTTP payload and call the server_call chain above.
// Eventually, login_callback will be called to let us know if the login was
// successful.
rc_client_begin_login_with_password(g_client, username, password,
login_callback, userdata);
}
void ra_login_token_user(const char *username, const char *token,
void *userdata) {
// This is exactly the same functionality as
// rc_client_begin_login_with_password, but uses the token captured from the
// first login instead of a password. Note that it uses the same callback.
rc_client_begin_login_with_token(g_client, username, token, login_callback,
userdata);
}
static void load_game_callback(int result, const char *error_message,
rc_client_t *client, void *userdata) {
if (result != RC_OK) {
if (client && !rc_client_get_userdata(client))
rc_client_set_userdata(client, strdup(error_message));
rc_client_set_hardcore_enabled(client, false);
notify_load_game(userdata);
return;
}
if (!rc_client_is_processing_required(client)) {
rc_client_set_hardcore_enabled(client, false);
}
g_leaderboard_list = rc_client_create_leaderboard_list(
client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE);
notify_load_game(userdata);
}
void ra_welcome() {
if (!g_client)
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);
free(error_message);
rc_client_set_userdata(g_client, NULL);
return;
}
const rc_client_game_t *game = rc_client_get_game_info(g_client);
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");
if (summary.num_core_achievements != 0) {
snprintf(buffer + message_length, sizeof(buffer) - message_length,
"%u/%u achievements unlocked", summary.num_unlocked_achievements,
summary.num_core_achievements);
} else {
snprintf(buffer + message_length, sizeof(buffer) - message_length,
"Game has no achievements");
}
rdp_onscreen_message(buffer, true);
}
void ra_load_game(const uint8_t *rom, size_t rom_size, void *userdata) {
rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO_64, NULL,
rom, rom_size, load_game_callback,
userdata);
}
void ra_unload_game() {
if (g_leaderboard_list) {
rc_client_destroy_leaderboard_list(g_leaderboard_list);
g_leaderboard_list = NULL;
}
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;
}
static void leaderboard_submitted(const rc_client_leaderboard_t *leaderboard) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "Leaderboard submitted: %s - %s",
leaderboard->title, leaderboard->tracker_value);
rdp_onscreen_message(buffer, false);
}
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);
}
static void game_completed(rc_client_t *client) {
char buffer[512];
const rc_client_game_t *game = rc_client_get_game_info(client);
snprintf(buffer, sizeof(buffer), "%s: %s",
rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",
game->title);
rdp_onscreen_message(buffer, false);
}
static void subset_completed(const rc_client_subset_t *subset,
rc_client_t *client) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "Subset %s: %s",
rc_client_get_hardcore_enabled(client) ? "mastered" : "completed",
subset->title);
rdp_onscreen_message(buffer, false);
}
static void server_error(const rc_client_server_error_t *server_error) {
char buffer[512];
snprintf(buffer, sizeof(buffer), "RA server error: %s",
server_error->error_message);
rdp_onscreen_message(buffer, false);
}
static const char *get_leaderboard_title(const char *display) {
if (g_leaderboard_list == NULL)
return NULL;
for (uint32_t i = 0; i < g_leaderboard_list->num_buckets; i++) {
for (uint32_t j = 0; j < g_leaderboard_list->buckets[i].num_leaderboards;
j++) {
// this looks like a mistake, but it is intentional.
// Leaderboard trackers don't contain the title,
// but we take advantage of the fact that tracker_value and display point
// to the same char* to find a matching leaderboard.
if (g_leaderboard_list->buckets[i].leaderboards[j]->tracker_value ==
display) {
return g_leaderboard_list->buckets[i].leaderboards[j]->title;
}
}
}
return NULL;
}
static void event_handler(const rc_client_event_t *event, rc_client_t *client) {
switch (event->type) {
case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED:
achievement_triggered(event->achievement);
break;
case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
break;
case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
break;
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
if (g_leaderboard)
leaderboard_submitted(event->leaderboard);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
if (g_challenge)
achievement_challenge_indicator_add(event->achievement->title);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
if (g_challenge)
achievement_challenge_indicator_remove(event->achievement->title);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW:
achievement_progress_add(event->achievement->title,
event->achievement->measured_progress);
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE:
achievement_progress_remove();
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE:
achievement_progress_add(event->achievement->title,
event->achievement->measured_progress);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW:
if (g_leaderboard)
leaderboard_tracker_add(
event->leaderboard_tracker->id,
get_leaderboard_title(event->leaderboard_tracker->display),
event->leaderboard_tracker->display);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE:
if (g_leaderboard)
leaderboard_tracker_remove(event->leaderboard_tracker->id);
break;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE:
if (g_leaderboard)
leaderboard_tracker_add(
event->leaderboard_tracker->id,
get_leaderboard_title(event->leaderboard_tracker->display),
event->leaderboard_tracker->display);
break;
case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD:
break;
case RC_CLIENT_EVENT_GAME_COMPLETED:
game_completed(client);
break;
case RC_CLIENT_EVENT_SUBSET_COMPLETED:
subset_completed(event->subset, client);
break;
case RC_CLIENT_EVENT_SERVER_ERROR:
server_error(event->server_error);
break;
default:
printf("RetroAchievements: Unhandled event %d\n", event->type);
break;
}
}
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;
// Provide a logging function to simplify debugging
rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_WARN, log_message);
rc_client_set_event_handler(g_client, event_handler);
rc_client_set_hardcore_enabled(g_client, hardcore);
g_challenge = challenge;
g_leaderboard = leaderboard;
}
bool ra_get_hardcore() { return rc_client_get_hardcore_enabled(g_client); }
void ra_shutdown_client() {
if (g_client) {
// Release resources associated to the client instance
rc_client_destroy(g_client);
g_client = NULL;
}
}
void ra_do_frame() { rc_client_do_frame(g_client); }
void ra_do_idle() { rc_client_idle(g_client); }
size_t ra_state_size() { return rc_client_progress_size(g_client); }
void ra_save_state(uint8_t *state, size_t state_size) {
if (!rc_client_is_game_loaded(g_client))
return;
if (rc_client_serialize_progress_sized(g_client, state, state_size) !=
RC_OK) {
printf("RetroAchievements: Failed to serialize progress\n");
}
}
void ra_load_state(const uint8_t *state, size_t state_size) {
if (!rc_client_is_game_loaded(g_client))
return;
if (rc_client_deserialize_progress_sized(g_client, state, state_size) !=
RC_OK) {
printf("RetroAchievements: Failed to deserialize progress\n");
}
}
void ra_display_inprogress_achievements(void *userdata) {
if (!rc_client_is_game_loaded(g_client))
return;
rc_client_achievement_list_t *list = rc_client_create_achievement_list(
g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE,
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE);
char buffer[1024] = {0};
size_t buffer_length = 0;
for (uint32_t i = 0; i < list->num_buckets; i++) {
if (list->buckets[i].bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED) {
for (uint32_t j = 0; j < list->buckets[i].num_achievements; j++) {
const rc_client_achievement_t *achievement =
list->buckets[i].achievements[j];
if (achievement->measured_percent > 0.0f) {
if (buffer_length < sizeof(buffer) - 1) {
buffer_length += snprintf(
buffer + buffer_length, sizeof(buffer) - buffer_length,
"%s: %s\n", achievement->title, achievement->measured_progress);
}
}
}
}
}
if (buffer_length > 0) {
rdp_onscreen_message(buffer, false);
}
rc_client_destroy_achievement_list(list);
}
+32
View File
@@ -0,0 +1,32 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
void ra_init_client(bool hardcore, bool challenge, bool leaderboard);
void ra_shutdown_client();
void ra_welcome();
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_do_frame();
void ra_do_idle();
void ra_http_callback(const char *content, size_t content_size, int status_code,
void *callback, void *callback_data);
void ra_logout_user();
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);
#ifdef __cplusplus
extern "C" {
#endif
void ra_display_inprogress_achievements(void *userdata);
#ifdef __cplusplus
}
#endif
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.88.0"
channel = "1.95.0"
+15 -7
View File
@@ -15,16 +15,24 @@ pub struct Cheats {
pub enabled: bool,
}
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
pub note: String,
pub data: Vec<String>,
pub options: Option<std::collections::BTreeMap<String, String>>,
}
pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
pub fn init(
device: &mut device::Device,
cheat_settings: std::collections::HashMap<String, Option<String>>,
) {
let cheats =
serde_json::from_slice::<ui::cheats::Cheats>(include_bytes!("../data/cheats.json"))
.unwrap()
.get(&ui::storage::get_game_crc(&device.cart.rom))
.cloned()
.unwrap_or_default();
let cheats = serde_json::from_slice::<Cheat>(include_bytes!("../data/cheats.json"))
.unwrap()
.get(&ui::storage::get_game_crc(&device.cart.rom))
.cloned()
.unwrap_or_default();
let re = regex::Regex::new(r"\?+").unwrap();
for cheat_setting in cheat_settings.iter() {
@@ -71,7 +79,7 @@ pub fn init(
if !device.cheats.cheats.is_empty() {
device.cheats.enabled = true;
ui::audio::play_cheat_event(&device.ui);
ui::video::onscreen_message("Cheats enabled", false);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
#include "src/compat/sse2neon/v1.8.0/sse2neon.h"
#include "src/compat/sse2neon/sse2neon.h"
// Static wrappers
Submodule src/compat/sse2neon added at b26afbb44d
File diff suppressed because it is too large Load Diff
+35 -24
View File
@@ -10,9 +10,9 @@ include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs"));
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use rand::{Rng, SeedableRng};
use crate::{cheats, netplay, ui};
use crate::{cheats, netplay, retroachievements, ui};
use std::{collections::HashMap, fs, io::Read};
pub mod ai;
@@ -44,24 +44,30 @@ pub mod tlb;
pub mod unmapped;
pub mod vi;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::gui::GameSettings) {
device.ui.video.fullscreen = game_settings.fullscreen;
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::GameSettings) {
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;
}
init_rng(device);
cart::rom::init(device, rom_contents); // cart needs to come before rdram
// rdram pointer is shared with parallel-rdp
// 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::input::init(&mut device.ui);
// must be after video init
retroachievements::welcome();
mi::init(device);
pif::init(device);
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
@@ -77,31 +83,34 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::g
ui::storage::init(&mut device.ui, &device.cart.rom);
ui::storage::load_saves(&mut device.ui, &mut device.netplay);
cheats::init(device, game_settings.cheats);
if !retroachievements::get_hardcore() {
cheats::init(device, game_settings.cheats);
}
cpu::run(device);
retroachievements::unload_game();
ui::storage::write_saves(device);
ui::input::close(&mut device.ui);
ui::audio::close(&mut device.ui);
ui::video::close(&device.ui);
}
fn set_rng() -> rand_chacha::ChaCha8Rng {
rand_chacha::ChaCha8Rng::from_os_rng()
fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
}
fn init_rng(device: &mut Device) {
let mut rng_seed = set_rng().next_u64();
if device.netplay.is_some() {
let netplay = device.netplay.as_mut().unwrap();
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
netplay::send_rng(netplay, rng_seed);
} else {
rng_seed = netplay::receive_rng(netplay);
}
}
device.rng = rand_chacha::ChaCha8Rng::seed_from_u64(rng_seed);
device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);
}
fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
@@ -132,7 +141,11 @@ fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
let mut contents = vec![];
if file_path.extension().unwrap().eq_ignore_ascii_case("zip") {
if file_path
.extension()
.unwrap_or_default()
.eq_ignore_ascii_case("zip")
{
let zip_file = fs::File::open(file_path).unwrap();
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
for i in 0..archive.len() {
@@ -149,7 +162,11 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
break;
}
}
} else if file_path.extension().unwrap().eq_ignore_ascii_case("7z") {
} else if file_path
.extension()
.unwrap_or_default()
.eq_ignore_ascii_case("7z")
{
let mut archive =
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
@@ -174,7 +191,8 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
)
.expect("ok");
} else {
contents = fs::read(file_path).expect("Should have been able to read the file");
contents = fs::read(file_path)
.unwrap_or_else(|_| panic!("Could not read ROM file: {}", file_path.display()));
}
if contents.is_empty() {
@@ -207,9 +225,8 @@ pub struct Device {
pub si: si::Si,
pub ri: ri::Ri,
#[serde(skip, default = "set_rng")]
pub rng: rand_chacha::ChaCha8Rng,
pub rng: rand::rngs::Xoshiro256PlusPlus,
pub vru: controller::vru::Vru,
pub vru_window: controller::vru::VruWindow,
pub transferpaks: [controller::transferpak::TransferPak; 4],
pub cheats: cheats::Cheats,
}
@@ -239,7 +256,6 @@ impl Device {
reg_write_masks: [0; cop0::COP0_REGS_COUNT as usize],
reg_latch: 0,
is_event: false,
pending_compare_interrupt: false,
instrs: [cop0::reserved; 32],
instrs2: [cop0::reserved; 32],
tlb_lut_w: vec![
@@ -347,6 +363,7 @@ impl Device {
sector: 0,
buffer: vec![0; 8192],
writeback_sector: vec![0; 256],
usb_buffer: vec![],
},
flashram: cart::sram::Flashram {
status: 0,
@@ -473,19 +490,13 @@ impl Device {
delay: 0,
count_per_scanline: 0,
enable_speed_limiter: true,
limiter: None,
next_pace_deadline: None,
vi_counter: 0,
last_origin: 0,
internal_frame_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(),
},
vru_window: controller::vru::VruWindow {
window_notifier: None,
word_receiver: None,
},
vru: controller::vru::Vru {
status: 0,
voice_state: 0,
+1 -2
View File
@@ -45,11 +45,10 @@ fn get_remaining_dma_length(device: &device::Device) -> u64 {
}
fn get_dma_duration(device: &device::Device) -> u64 {
let samples_per_sec = device.vi.clock / (1 + device.ai.regs[AI_DACRATE_REG as usize]) as 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;
length * (device.cpu.clock_rate / (bytes_per_sample * samples_per_sec))
(length * device.cpu.clock_rate) / (bytes_per_sample * device.ai.freq)
}
fn do_dma(device: &mut device::Device) {
+1 -11
View File
@@ -21,16 +21,6 @@ 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)]
pub enum CicType {
CicNus6101,
CicNus6102,
CicNus6103,
CicNus6105,
CicNus6106,
CicNus5167,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct AfRtc {
pub control: u16,
@@ -157,7 +147,7 @@ fn eeprom_write_block(device: &mut device::Device, block: usize, offset: usize,
device.pif.ram[status] = 0x00;
device.ui.storage.saves.eeprom.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
}
fn time2data(device: &mut device::Device, offset: usize) {
+12 -8
View File
@@ -1,8 +1,9 @@
use crate::device;
use crate::netplay;
use crate::ui;
use sha2::{Digest, Sha256};
const CART_MASK: usize = 0xFFFFFFF;
pub const CART_MASK: usize = 0xFFFFFFF;
fn read_cart_word(device: &device::Device, address: usize) -> u32 {
let mut data: [u8; 4] = [0; 4];
@@ -63,7 +64,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
.data
.insert((masked_address + i) as u32, *item);
}
device.ui.storage.saves.romsave.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Romsave);
}
device.cart.latch = value & mask;
@@ -94,7 +95,7 @@ pub fn dma_read(
.unwrap_or(&0),
);
}
device.ui.storage.saves.romsave.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Romsave);
}
device::pi::calculate_cycles(device, 1, length)
@@ -132,11 +133,10 @@ pub fn dma_write(
device::pi::calculate_cycles(device, 1, length)
}
pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
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 device.netplay.is_some() {
let netplay = device.netplay.as_mut().unwrap();
if let Some(netplay) = &mut device.netplay {
if netplay.player_number == 0 {
netplay::send_rtc(netplay, device.cart.rtc_timestamp);
} else {
@@ -146,7 +146,7 @@ pub fn init(device: &mut device::Device, rom_file: Vec<u8>) {
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 1;
device.cart.rom = rom_file;
device.cart.rom = rom_file.to_vec();
device.cart.pal = is_system_pal(device.cart.rom[0x3E]);
set_cic(device);
@@ -200,5 +200,9 @@ fn set_cic(device: &mut device::Device) {
pub fn calculate_hash(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:X}", hasher.finalize())
hasher
.finalize()
.iter()
.map(|b| format!("{:02X}", b))
.collect()
}
+130 -5
View File
@@ -28,6 +28,7 @@ pub struct Sc64 {
pub cfg: [u32; SC64_CFG_COUNT as usize],
pub sector: u32,
pub writeback_sector: Vec<u32>,
pub usb_buffer: Vec<u8>,
}
fn format_sdcard(device: &mut device::Device) {
@@ -207,10 +208,134 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
.copy_from_slice(&data);
i += 4;
}
device.ui.storage.saves.sdcard.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sdcard);
}
'U' => {
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 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] =
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
}
}
}
}
} 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
}
}
'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;
if let Some(usb_tx) = device.ui.usb.usb_tx.as_mut() {
let mut usb_buffer = vec![0; length];
let mut i = 0;
if address < device.rdram.size as u64 {
while i < length {
*usb_buffer.get_mut(i).unwrap_or(&mut 0) = *device
.rdram
.mem
.get((address as usize + i) ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
}
} else if address >= device::memory::MM_CART_ROM as u64
&& address < device::memory::MM_PIF_MEM as u64
{
while i < length {
*usb_buffer.get_mut(i).unwrap_or(&mut 0) = *device
.ui
.storage
.saves
.romsave
.data
.get(
&(((address as usize + i)
& device::cart::rom::CART_MASK)
as u32),
)
.unwrap_or(
device
.cart
.rom
.get(
(address as usize + i)
& device::cart::rom::CART_MASK,
)
.unwrap_or(&0),
);
i += 1;
}
} else {
panic!("Unknown address {address:#x} for SC64 M command");
}
ui::usb::send_to_usb(
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,
},
);
}
}
'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 mut i = 0;
if address < device.rdram.size as u64 {
while i < length {
*device
.rdram
.mem
.get_mut((address as usize + i) ^ device.byte_swap)
.unwrap_or(&mut 0) =
*device.cart.sc64.usb_buffer.get(i).unwrap_or(&0);
i += 1;
}
} else if address >= device::memory::MM_CART_ROM as u64
&& address < device::memory::MM_PIF_MEM as u64
{
while i < length {
device.ui.storage.saves.romsave.data.insert(
((address as usize + i) & device::cart::rom::CART_MASK) as u32,
*device.cart.sc64.usb_buffer.get(i).unwrap_or(&0),
);
i += 1;
}
} else {
panic!("Unknown address {address:#x} for SC64 m command");
}
}
'U' | 'u' => {} // USB status, ignored
'M' | 'm' => {} // USB read/write, ignored
'w' => {
// SD card writeback pending
device.cart.sc64.regs[SC64_DATA0_REG as usize] = 0;
@@ -280,7 +405,7 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
device::memory::masked_write_32(&mut data, value, mask);
device.ui.storage.saves.eeprom.data[masked_address..masked_address + 4]
.copy_from_slice(&data.to_be_bytes());
device.ui.storage.saves.eeprom.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
} else {
let masked_address = address as usize & SC64_BUFFER_MASK;
let mut data = u32::from_be_bytes(
@@ -304,7 +429,7 @@ pub fn dma_read(
let buffer = if cart_addr & 0x2000 != 0 {
device::cart::format_eeprom(device);
cart_addr &= SC64_EEPROM_MASK as u32;
device.ui.storage.saves.eeprom.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Eeprom4k);
&mut device.ui.storage.saves.eeprom.data
} else {
cart_addr &= SC64_BUFFER_MASK as u32;
+5 -5
View File
@@ -107,7 +107,7 @@ fn write_mem_sram(device: &mut device::Device, address: u64, value: u32, mask: u
device.ui.storage.saves.sram.data[masked_address..masked_address + 4]
.copy_from_slice(&data.to_be_bytes());
device.ui.storage.saves.sram.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sram);
}
fn write_mem_flash(device: &mut device::Device, address: u64, value: u32, mask: u32) {
@@ -167,7 +167,7 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
j += 1;
}
device.ui.storage.saves.sram.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Sram);
}
fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, length: u32) {
@@ -331,12 +331,12 @@ fn flashram_command(device: &mut device::Device, command: u32) {
for i in 0..128 * 128 {
device.ui.storage.saves.flash.data[offset + i] = 0xFF;
}
device.ui.storage.saves.flash.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
} else if device.cart.flashram.mode == FlashramMode::ChipErase {
for i in 0..FLASHRAM_SIZE {
device.ui.storage.saves.flash.data[i] = 0xFF;
}
device.ui.storage.saves.flash.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
} else {
panic!("Unexpected flash erase command")
}
@@ -356,7 +356,7 @@ fn flashram_command(device: &mut device::Device, command: u32) {
for i in 0..128 {
device.ui.storage.saves.flash.data[offset + i] = device.cart.flashram.page_buf[i];
}
device.ui.storage.saves.flash.written = true;
ui::storage::schedule_save(device, ui::storage::SaveTypes::Flash);
/* clear program busy flag, set program success flag, transition to status mode */
device.cart.flashram.status &= !0x01;
+13 -10
View File
@@ -56,15 +56,14 @@ pub fn process(device: &mut device::Device, channel: usize) {
}
JCMD_CONTROLLER_READ => {
let offset = device.pif.channels[channel].rx_buf.unwrap();
let input = if device.netplay.is_none() {
ui::input::get(&device.ui, channel)
} else {
if device.netplay.as_ref().unwrap().player_number as usize == channel {
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(device.netplay.as_ref().unwrap(), local_input);
netplay::send_input(netplay, local_input);
}
netplay::get_input(device, channel)
} else {
ui::input::get(&device.ui, channel)
};
device.pif.ram[offset..offset + 4].copy_from_slice(&input.data.to_ne_bytes());
@@ -162,10 +161,12 @@ pub fn pak_switch_event(device: &mut device::Device) {
for (i, channel) in device.pif.channels.iter_mut().enumerate() {
if channel.change_pak != PakType::None {
//stop rumble if it is on
if device.netplay.is_none() {
if let Some(netplay) = &device.netplay {
if netplay.player_number as usize == i {
device::ui::input::set_rumble(&device.ui, 0, 0);
}
} else {
device::ui::input::set_rumble(&device.ui, i, 0);
} else if device.netplay.as_ref().unwrap().player_number as usize == i {
device::ui::input::set_rumble(&device.ui, 0, 0);
}
let new_pak_type = match channel.change_pak {
@@ -189,20 +190,22 @@ 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);
} 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);
} 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::audio::play_pak_switch(&device.ui, new_pak_type);
channel.change_pak = PakType::None;
}
}
+7 -11
View File
@@ -110,7 +110,7 @@ fn write_mbc3(
if address < 0x2000 {
cart.ram_enabled = value & 0xf == 0xa;
} else if address < 0x4000 {
let bank = value & 0x7f;
let bank = value & if cart.rom.len() > 2097152 { 0xff } else { 0x7f };
cart.rom_bank = bank as u32;
if cart.rom_bank == 0 {
cart.rom_bank = 1;
@@ -182,16 +182,12 @@ fn read_mbc3(
return;
}
if cart.ram_bank < 0x8 {
if cart.ram_bank > 3 {
for i in 0..size {
pif_ram[data + i] = 0;
}
} else {
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
pif_ram[data..data + size].copy_from_slice(
&cart.ram[banked_address as usize..banked_address as usize + size],
);
}
let banked_address = address - 0xA000 + (cart.ram_bank * 0x2000);
pif_ram[data..data + size].copy_from_slice(
cart.ram
.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,
+2 -1
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::ui;
pub const MEMPAK_SIZE: usize = 0x8000;
const MPK_PAGE_SIZE: usize = 256;
@@ -96,6 +97,6 @@ pub fn write(device: &mut device::Device, channel: usize, address: u16, data: us
device.ui.storage.saves.mempak.data[offset..offset + size]
.copy_from_slice(&device.pif.ram[data..data + size]);
device.ui.storage.saves.mempak.written = true
ui::storage::schedule_save(device, ui::storage::SaveTypes::Mempak);
}
}
+6 -3
View File
@@ -15,10 +15,13 @@ pub fn read(device: &mut device::Device, _channel: usize, address: u16, data: us
pub fn write(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
if address == 0xc000 {
let rumble = device.pif.ram[data + size - 1];
if device.netplay.is_none() {
if let Some(netplay) = &device.netplay {
if netplay.player_number as usize == channel {
device::ui::input::set_rumble(&device.ui, 0, rumble);
}
} else {
device::ui::input::set_rumble(&device.ui, channel, rumble);
} else if device.netplay.as_ref().unwrap().player_number as usize == channel {
device::ui::input::set_rumble(&device.ui, 0, rumble);
}
}
}
+7 -18
View File
@@ -1,4 +1,6 @@
use crate::{device, ui};
use crate::device;
#[cfg(feature = "gui")]
use crate::ui;
use std::collections::HashMap;
const JCMD_VRU_READ: u8 = 0x09;
@@ -29,14 +31,6 @@ pub struct Vru {
pub word_mappings: HashMap<String, String>,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct VruWindow {
#[serde(skip)]
pub window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
#[serde(skip)]
pub word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
}
pub fn init(device: &mut device::Device) {
reset_vru(device);
create_word_mappings(device);
@@ -187,15 +181,10 @@ pub fn process(device: &mut device::Device, channel: usize) {
device.pif.ram[device.pif.channels[channel].rx_buf.unwrap()] = 0;
}
JCMD_VRU_READ => {
let index = if device.vru_window.window_notifier.is_some() {
ui::vru::prompt_for_match(
&device.vru.words,
device.vru_window.window_notifier.as_ref().unwrap(),
device.vru_window.word_receiver.as_mut().unwrap(),
)
} else {
0x7FFF
};
#[cfg(feature = "gui")]
let index = ui::vru::prompt_for_match(&device.vru.words, device.vi.frame_time);
#[cfg(not(feature = "gui"))]
let index = 0x7FFF;
let num_results = if index == 0x7FFF { 0 } else { 1 };
let data: HashMap<usize, u16> = HashMap::from([
(0, 0x8000),
+47 -41
View File
@@ -30,13 +30,15 @@ pub const COP0_XCONTEXT_REG: u32 = 20;
//const COP0_CACHEERR_REG: u32 = 27;
pub const COP0_TAGLO_REG: u32 = 28;
//const COP0_TAGHI_REG: u32 = 29;
const COP0_ERROREPC_REG: u32 = 30;
pub const COP0_ERROREPC_REG: u32 = 30;
//const COP0_UNUSED_31: u32 = 31;
pub const COP0_REGS_COUNT: u32 = 32;
pub const COP0_STATUS_IE: u64 = 1 << 0;
pub const COP0_STATUS_EXL: u64 = 1 << 1;
pub const COP0_STATUS_ERL: u64 = 1 << 2;
pub const COP0_STATUS_SR: u64 = 1 << 20;
pub const COP0_STATUS_TS: u64 = 1 << 21;
pub const COP0_STATUS_BEV: u64 = 1 << 22;
pub const COP0_STATUS_FR: u64 = 1 << 26;
pub const COP0_STATUS_CU1: u64 = 1 << 29;
@@ -55,6 +57,7 @@ pub const COP0_CAUSE_EXCCODE_CPU: u64 = 11 << 2;
pub const COP0_CAUSE_EXCCODE_TR: u64 = 13 << 2;
pub const COP0_CAUSE_EXCCODE_FPE: u64 = 15 << 2;
pub const COP0_CAUSE_IP2: u64 = 1 << 10;
pub const COP0_CAUSE_IP4: u64 = 1 << 12;
pub const COP0_CAUSE_IP7: u64 = 1 << 15;
pub const COP0_CAUSE_BD: u64 = 1 << 31;
@@ -62,7 +65,7 @@ pub const COP0_CAUSE_CE1: u64 = 1 << 28;
pub const COP0_CAUSE_CE2: u64 = 1 << 29;
pub const COP0_CAUSE_IP_MASK: u64 = 0b00000000000000001111111100000000;
//pub const COP0_CAUSE_EXCCODE_MASK: u64 = 0x1F << 2;
pub const COP0_CAUSE_EXCCODE_MASK: u64 = 0x1F << 2;
pub const COP0_CONTEXT_BADVPN2_MASK: u64 = 0b00000000011111111111111111110000;
pub const COP0_XCONTEXT_BADVPN2_MASK: u64 = 0b01111111111111111111111111110000;
pub const COP0_XCONTEXT_REGION_MASK: u64 = 0b110000000000000000000000000000000;
@@ -101,7 +104,6 @@ pub struct Cop0 {
pub tlb_lut_w: Vec<device::tlb::TlbLut>,
pub tlb_entries: [device::tlb::TlbEntry; 32],
pub is_event: bool,
pub pending_compare_interrupt: bool,
}
fn mfc0(device: &mut device::Device, opcode: u32) {
@@ -179,6 +181,8 @@ pub fn reserved(device: &mut device::Device, _opcode: u32) {
device::exceptions::reserved_exception(device, 0);
}
pub fn emux_xdetect(_device: &mut device::Device, _opcode: u32) {}
fn get_control_registers(device: &device::Device, index: u32) -> u64 {
match index {
COP0_COUNT_REG => device.cpu.cop0.regs[index as usize] >> 1,
@@ -218,12 +222,11 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
(compare_event_diff as u64) << 1,
);
device.cpu.cop0.regs[COP0_CAUSE_REG as usize] &= !COP0_CAUSE_IP7;
device.cpu.cop0.pending_compare_interrupt = false;
}
COP0_STATUS_REG => {
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR {
device::cop1::set_fgr_registers(device, data)
}
COP0_STATUS_REG
if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR =>
{
device::cop1::set_fgr_registers(device, data)
}
_ => {}
}
@@ -236,7 +239,10 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64)
}
pub fn compare_event(device: &mut device::Device) {
device.cpu.cop0.pending_compare_interrupt = true;
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::events::create_event_at(
device,
device::events::EVENT_TYPE_COMPARE,
@@ -261,38 +267,38 @@ pub fn add_cycles(device: &mut device::Device, cycles: u64) {
pub fn map_instructions(device: &mut device::Device) {
device.cpu.cop0.instrs = [
device::cop0::mfc0, // 0
device::cop0::dmfc0, // 1
device::cop0::reserved, // 2
device::cop0::reserved, // 3
device::cop0::mtc0, // 4
device::cop0::dmtc0, // 5
device::cop0::reserved, // 6
device::cop0::reserved, // 7
device::cop0::reserved, // 8
device::cop0::reserved, // 9
device::cop0::reserved, // 10
device::cop0::reserved, // 11
device::cop0::reserved, // 12
device::cop0::reserved, // 13
device::cop0::reserved, // 14
device::cop0::reserved, // 15
device::cop0::execute_cp0, // 16
device::cop0::reserved, // 17
device::cop0::reserved, // 18
device::cop0::reserved, // 19
device::cop0::reserved, // 20
device::cop0::reserved, // 21
device::cop0::reserved, // 22
device::cop0::reserved, // 23
device::cop0::reserved, // 24
device::cop0::reserved, // 25
device::cop0::reserved, // 26
device::cop0::reserved, // 27
device::cop0::reserved, // 28
device::cop0::reserved, // 29
device::cop0::reserved, // 30
device::cop0::reserved, // 31
device::cop0::mfc0, // 0
device::cop0::dmfc0, // 1
device::cop0::reserved, // 2
device::cop0::reserved, // 3
device::cop0::mtc0, // 4
device::cop0::dmtc0, // 5
device::cop0::reserved, // 6
device::cop0::reserved, // 7
device::cop0::reserved, // 8
device::cop0::reserved, // 9
device::cop0::reserved, // 10
device::cop0::reserved, // 11
device::cop0::reserved, // 12
device::cop0::reserved, // 13
device::cop0::reserved, // 14
device::cop0::reserved, // 15
device::cop0::execute_cp0, // 16
device::cop0::reserved, // 17
device::cop0::reserved, // 18
device::cop0::reserved, // 19
device::cop0::emux_xdetect, // 20
device::cop0::reserved, // 21
device::cop0::reserved, // 22
device::cop0::reserved, // 23
device::cop0::reserved, // 24
device::cop0::reserved, // 25
device::cop0::reserved, // 26
device::cop0::reserved, // 27
device::cop0::reserved, // 28
device::cop0::reserved, // 29
device::cop0::reserved, // 30
device::cop0::reserved, // 31
];
device.cpu.cop0.instrs2 = [
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::{device, savestates};
use crate::{device, savestates, ui};
#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
pub enum State {
@@ -227,7 +227,7 @@ pub fn init(device: &mut device::Device) {
device.cpu.clock_rate = if !device.cpu.overclock {
93750000
} else {
println!("Overclocking enabled, setting clock rate to 125 MHz");
ui::video::onscreen_message("Overclocking enabled, setting clock rate to 125 MHz", false);
125000000
};
+6 -1
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::ui;
pub const EVENT_TYPE_NONE: usize = 0;
pub const EVENT_TYPE_AI: usize = 1;
@@ -12,7 +13,9 @@ pub const EVENT_TYPE_SPDMA: usize = 8;
pub const EVENT_TYPE_COMPARE: usize = 9;
pub const EVENT_TYPE_VRU: usize = 10;
pub const EVENT_TYPE_PAK: usize = 11;
pub const EVENT_TYPE_COUNT: usize = 12;
pub const EVENT_TYPE_SAVE: usize = 12;
pub const EVENT_TYPE_NMI: usize = 13;
pub const EVENT_TYPE_COUNT: usize = 14;
#[derive(PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub struct Event {
@@ -60,6 +63,8 @@ fn get_event_handler(name: usize) -> fn(&mut device::Device) {
EVENT_TYPE_COMPARE => device::cop0::compare_event,
EVENT_TYPE_VRU => device::controller::vru::vru_talking_event,
EVENT_TYPE_PAK => device::controller::pak_switch_event,
EVENT_TYPE_SAVE => ui::storage::save_event,
EVENT_TYPE_NMI => device::exceptions::reset_event,
_ => dummy_event,
}
}
+22 -4
View File
@@ -1,4 +1,5 @@
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]
@@ -15,10 +16,9 @@ pub fn check_pending_interrupts(device: &mut device::Device) {
& 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;
} else if device.cpu.cop0.pending_compare_interrupt {
device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG as usize] = device::cop0::COP0_CAUSE_IP7;
device.cpu.cop0.pending_compare_interrupt = false;
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] &=
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
}
if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG as usize]
@@ -151,6 +151,24 @@ pub fn tlb_miss_exception(
exception_general(device, vector_offset)
}
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_STATUS_REG as usize] |= 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_ERROREPC_REG as usize] = 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::pif::reset_pif(device, true);
ui::video::onscreen_message("Game reset", false);
}
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
+2 -2
View File
@@ -22,8 +22,8 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
let data = std::str::from_utf8(
&device.cart.is_viewer_buffer[0x20_usize..(0x20 + length) as usize],
);
if data.is_ok() {
print!("{}", data.unwrap());
if let Ok(data) = data {
print!("{}", data);
}
} else {
let mut data = u32::from_be_bytes(
+7 -2
View File
@@ -62,11 +62,16 @@ pub fn masked_write_64(dst: &mut u64, value: u64, mask: u64) {
pub fn translate_address(
device: &mut device::Device,
address: u64,
mut address: u64,
access_type: AccessType,
) -> (u64, bool, bool) {
if (address & 0xc0000000) != 0x80000000 {
return device::tlb::get_physical_address(device, address, access_type);
if (address & 0xFFFFFFFF00000000) == 0x9000000000000000 {
// this is a hack to support 64-bit addresses used by some homebrew
address |= 0xA0000000;
} else {
return device::tlb::get_physical_address(device, address, access_type);
}
}
(address & 0x1FFFFFFF, address & 0x20000000 == 0, false)
}
+9 -3
View File
@@ -1,6 +1,6 @@
use crate::device;
const MI_INIT_MODE_REG: u32 = 0;
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;
@@ -29,7 +29,7 @@ const MI_CLR_DP: u32 = 1 << 10;
const MI_SET_DP: u32 = 1 << 11;
/* mode read */
const MI_INIT_MODE: u32 = 1 << 7;
pub const MI_INIT_MODE: u32 = 1 << 7;
const MI_EBUS_MODE: u32 = 1 << 8;
const MI_RDRAM_MODE: u32 = 1 << 9;
@@ -42,7 +42,7 @@ const MI_CLR_DP_INTR: u32 = 1 << 11;
const MI_CLR_RDRAM: u32 = 1 << 12;
const MI_SET_RDRAM: u32 = 1 << 13;
const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
pub const MI_INIT_LENGTH_MASK: u32 = 0b1111111;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Mi {
@@ -78,9 +78,15 @@ fn update_init_mode(device: &mut device::Device, w: u32) {
device.mi.regs[MI_INIT_MODE_REG as usize] |= 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
}
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
}
if w & MI_CLR_EBUS != 0 {
+6 -2
View File
@@ -1,5 +1,6 @@
use crate::device;
use rand_chacha::rand_core::RngCore;
use crate::ui;
use rand::Rng;
const PI_DRAM_ADDR_REG: u32 = 0;
const PI_CART_ADDR_REG: u32 = 1;
@@ -92,6 +93,9 @@ fn dma_write(device: &mut device::Device) {
length -= dram_addr & 0x7;
}
// check RDP before PI writes to RDRAM
ui::video::check_framebuffers(dram_addr, length);
let cycles = (handler.write)(device, cart_addr, dram_addr, length);
device::events::create_event(device, device::events::EVENT_TYPE_PI, cycles);
@@ -165,7 +169,7 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
}
fn randomize_interrupt_time(rng: &mut rand_chacha::ChaCha8Rng) -> u64 {
fn randomize_interrupt_time(rng: &mut rand::rngs::Xoshiro256PlusPlus) -> u64 {
rng.next_u64() % 0x10
}
+24 -15
View File
@@ -218,11 +218,11 @@ pub fn process_ram(device: &mut device::Device) {
pub fn connect_pif_channels(device: &mut device::Device) {
for i in 0..4 {
if device.netplay.is_none() {
if device.ui.config.input.controller_enabled[i] {
if let Some(netplay) = &device.netplay {
if netplay.player_data[i].reg_id != 0 {
device.pif.channels[i].process = Some(device::controller::process);
}
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
} else if device.ui.config.input.controller_enabled[i] {
device.pif.channels[i].process = Some(device::controller::process);
}
}
@@ -249,14 +249,23 @@ fn get_default_handler(device: &device::Device) -> device::controller::PakHandle
}
}
pub fn init(device: &mut device::Device) {
pub fn reset_pif(device: &mut device::Device, is_nmi_reset: bool) {
if device.cart.pal {
device.pif.rom = rom::PAL_PIF_ROM;
} else {
device.pif.rom = rom::NTSC_PIF_ROM;
}
device.pif.ram[0x26] = device.cart.cic_seed;
device.pif.ram[0x27] = device.cart.cic_seed;
let reset_type = u32::from(is_nmi_reset);
let rom_type = 0u32;
let s7 = 0u32;
let cic = u32::from(device.cart.cic_seed);
let word = (rom_type << 19) | (s7 << 18) | (reset_type << 17) | (cic << 8) | 0x3f;
device.pif.ram[0x24..0x28].copy_from_slice(&word.to_be_bytes());
}
pub fn init(device: &mut device::Device) {
reset_pif(device, false);
let default_handler = get_default_handler(device);
let tpak_handler = device::controller::PakHandler {
@@ -268,16 +277,16 @@ pub fn init(device: &mut device::Device) {
connect_pif_channels(device);
for i in 0..4 {
if device.netplay.is_none() {
if device.ui.config.input.controller_enabled[i] {
if device.transferpaks[i].cart.rom.is_empty() {
device.pif.channels[i].pak_handler = Some(default_handler);
} else {
device.pif.channels[i].pak_handler = Some(tpak_handler);
}
if let Some(netplay) = &device.netplay {
if netplay.player_data[i].reg_id != 0 {
device.pif.channels[i].pak_handler = Some(default_handler);
}
} else if device.ui.config.input.controller_enabled[i] {
if device.transferpaks[i].cart.rom.is_empty() {
device.pif.channels[i].pak_handler = Some(default_handler);
} else {
device.pif.channels[i].pak_handler = Some(tpak_handler);
}
} else if device.netplay.as_ref().unwrap().player_data[i].reg_id != 0 {
device.pif.channels[i].pak_handler = Some(default_handler);
}
}
if device.ui.config.input.emulate_vru && device.netplay.is_none() {
+41
View File
@@ -1,4 +1,6 @@
use crate::device;
use crate::retroachievements;
use crate::ui;
use std::alloc::{Layout, alloc_zeroed};
//const RDRAM_CONFIG_REG: u32 = 0;
@@ -50,6 +52,8 @@ pub fn read_mem(
);
let masked_address = address as usize & RDRAM_MASK;
ui::video::check_framebuffers(masked_address as u32, 4);
u32::from_ne_bytes(
device
.rdram
@@ -62,6 +66,8 @@ pub fn read_mem(
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
ui::video::check_framebuffers(address as u32, 4);
let mut data = u32::from_ne_bytes(
device
.rdram
@@ -80,6 +86,39 @@ pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u3
.copy_from_slice(&data.to_ne_bytes());
}
pub fn write_mem_repeat(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if mask != 0xFFFFFFFF {
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;
if !repeat_length.is_multiple_of(4) {
panic!(
"RDRAM write_mem_repeat called with non-word-aligned length {}",
repeat_length
);
}
ui::video::check_framebuffers(address as u32, repeat_length);
for i in (0..repeat_length as u64).step_by(4) {
device
.rdram
.mem
.get_mut((address + i) as usize..(address + i) as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&value.to_ne_bytes());
}
device.mi.regs[device::mi::MI_INIT_MODE_REG as usize] &= !device::mi::MI_INIT_MODE;
for i in 0..(0x3F00000 >> 16) {
device.memory.memory_map_write[i] = device::rdram::write_mem;
}
}
pub fn read_regs(
device: &mut device::Device,
address: u64,
@@ -113,6 +152,8 @@ 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);
// hack, skip RDRAM initialization
device
.rdram
+14 -10
View File
@@ -1,4 +1,5 @@
use crate::device;
use crate::ui;
const SP_MEM_ADDR_REG: u32 = 0;
const SP_DRAM_ADDR_REG: u32 = 1;
@@ -146,6 +147,7 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
let mut dram_addr = dma.dramaddr & 0xfffff8;
let offset = dma.memaddr & 0x1000;
ui::video::check_framebuffers(dram_addr, count * (length + skip) - skip);
if dma.dir == DmaDir::Read {
let mut j = 0;
while j < count {
@@ -219,6 +221,8 @@ fn fifo_push(device: &mut device::Device, dir: DmaDir) {
panic!("RSP DMA already full")
}
device.rsp.cpu.sync_point = true;
if device.rsp.regs[SP_DMA_BUSY_REG as usize] != 0 {
device.rsp.fifo[1].dir = dir;
if dir == DmaDir::Read {
@@ -276,21 +280,20 @@ pub fn read_regs(
}
let reg = (address & 0xFFFF) >> 2;
match reg as u32 {
SP_DMA_BUSY_REG | SP_DMA_FULL_REG => {
if device.rsp.regs[reg as usize] != 0 {
device.rsp.cpu.sync_point = true;
}
device.rsp.regs[reg as usize]
}
SP_STATUS_REG => {
let value = device.rsp.regs[reg as usize]
& !(SP_STATUS_HALT | SP_STATUS_BROKE | SP_STATUS_INTR_BREAK);
& (SP_STATUS_SIG0
| SP_STATUS_SIG1
| SP_STATUS_SIG2
| SP_STATUS_SIG3
| SP_STATUS_SIG4
| SP_STATUS_SIG5
| SP_STATUS_SIG6
| SP_STATUS_SIG7);
if value == device.rsp.last_status_value && value != 0 {
device.rsp.cpu.sync_point = true;
}
if value & (SP_STATUS_DMA_BUSY | SP_STATUS_DMA_FULL) != 0 {
device.rsp.cpu.sync_point = true;
}
device.rsp.last_status_value = value;
device.rsp.regs[reg as usize]
}
@@ -464,6 +467,7 @@ fn update_sp_status(device: &mut device::Device, w: u32) {
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 {
device.rsp.run_after_dma = true
} else {
+138 -43
View File
@@ -304,25 +304,44 @@ pub fn lh(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 2];
w[0] = device.rsp.mem[address as usize & 0xFFF];
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFE {
u16::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 2]
.try_into()
.unwrap(),
)
} else {
let w0 = device.rsp.mem[addr_masked];
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
((w0 as u16) << 8) | w1 as u16
};
device.rsp.cpu.gpr[rt(opcode) as usize] =
(((w[0] as u16) << 8) | w[1] as u16) as i16 as i32 as u32
device.rsp.cpu.gpr[rt(opcode) as usize] = value as i16 as i32 as u32;
}
pub fn lw(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 4];
for (i, item) in w.iter_mut().enumerate() {
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFC {
u32::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 4]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
}
value
};
device.rsp.cpu.gpr[rt(opcode) as usize] =
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
}
pub fn lbu(device: &mut device::Device, opcode: u32) {
@@ -336,24 +355,44 @@ pub fn lhu(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 2];
w[0] = device.rsp.mem[address as usize & 0xFFF];
w[1] = device.rsp.mem[(address as usize + 1) & 0xFFF];
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFE {
u16::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 2]
.try_into()
.unwrap(),
)
} else {
let w0 = device.rsp.mem[addr_masked];
let w1 = device.rsp.mem[(addr_masked + 1) & 0xFFF];
((w0 as u16) << 8) | w1 as u16
};
device.rsp.cpu.gpr[rt(opcode) as usize] = (((w[0] as u16) << 8) | w[1] as u16) as u32
device.rsp.cpu.gpr[rt(opcode) as usize] = value as u32;
}
pub fn lwu(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
let mut w = [0; 4];
for (i, item) in w.iter_mut().enumerate() {
*item = device.rsp.mem[(address as usize + i) & 0xFFF];
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFC {
u32::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 4]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
}
value
};
device.rsp.cpu.gpr[rt(opcode) as usize] =
((w[0] as u32) << 24) | ((w[1] as u32) << 16) | ((w[2] as u32) << 8) | (w[3] as u32)
device.rsp.cpu.gpr[rt(opcode) as usize] = value;
}
pub fn sb(device: &mut device::Device, opcode: u32) {
@@ -367,18 +406,30 @@ pub fn sh(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
device.rsp.mem[address as usize & 0xFFF] = (device.rsp.cpu.gpr[rt(opcode) as usize] >> 8) as u8;
device.rsp.mem[(address as usize + 1) & 0xFFF] =
(device.rsp.cpu.gpr[rt(opcode) as usize]) as u8;
let addr_masked = address as usize & 0xFFF;
let value = device.rsp.cpu.gpr[rt(opcode) as usize] as u16;
// Optimize for aligned access when possible
if addr_masked <= 0xFFE {
device.rsp.mem[addr_masked..addr_masked + 2].copy_from_slice(&value.to_be_bytes());
} else {
device.rsp.mem[addr_masked] = (value >> 8) as u8;
device.rsp.mem[(addr_masked + 1) & 0xFFF] = value as u8;
}
}
pub fn sw(device: &mut device::Device, opcode: u32) {
let address =
device.rsp.cpu.gpr[rs(opcode) as usize].wrapping_add(imm(opcode) as i16 as i32 as u32);
for i in 0..4 {
device.rsp.mem[(address as usize + i) & 0xFFF] =
(device.rsp.cpu.gpr[rt(opcode) as usize] >> ((3 - i) * 8)) as u8;
let addr_masked = address as usize & 0xFFF;
let value = device.rsp.cpu.gpr[rt(opcode) as usize];
// Optimize for aligned access when possible
if addr_masked <= 0xFFC {
device.rsp.mem[addr_masked..addr_masked + 4].copy_from_slice(&value.to_be_bytes());
} else {
for i in 0..4 {
device.rsp.mem[(addr_masked + i) & 0xFFF] = (value >> ((3 - i) * 8)) as u8;
}
}
}
@@ -737,10 +788,21 @@ pub fn llv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element.is_multiple_of(4) {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u32) << (8 * (3 - i));
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFFC {
u32::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 4]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..4 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u32) << (8 * (3 - i));
}
value
};
modify_vpr32(
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
element / 4,
@@ -767,10 +829,21 @@ pub fn ldv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element.is_multiple_of(8) {
let mut value = 0;
for i in 0..8 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u64) << (8 * (7 - i));
}
// Optimize for aligned access when possible
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFF8 {
u64::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 8]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..8 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u64) << (8 * (7 - i));
}
value
};
modify_vpr64(
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
element / 8,
@@ -797,10 +870,21 @@ pub fn lqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address.is_multiple_of(16) {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
}
// Optimize for aligned 16-byte access
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFF0 {
u128::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 16]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
}
value
};
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
} else {
let end = std::cmp::min(16 + element - ((address & 15) as u8), 16);
@@ -823,10 +907,21 @@ pub fn lrv(device: &mut device::Device, opcode: u32) {
let mut element = 16u8.wrapping_sub(((address & 15) as u8).wrapping_sub(velement(opcode)));
address &= !15;
if element == 0 {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[((address + i) & 0xFFF) as usize] as u128) << (8 * (15 - i));
}
// Optimize for aligned 16-byte access
let addr_masked = address as usize & 0xFFF;
let value = if addr_masked <= 0xFF0 {
u128::from_be_bytes(
device.rsp.mem[addr_masked..addr_masked + 16]
.try_into()
.unwrap(),
)
} else {
let mut value = 0;
for i in 0..16 {
value |= (device.rsp.mem[(addr_masked + i) & 0xFFF] as u128) << (8 * (15 - i));
}
value
};
modify_vpr128(&mut device.rsp.cpu.vpr[rt(opcode) as usize], value);
} else {
while element < 16 {
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -1,6 +1,6 @@
use rand_chacha::rand_core::RngCore;
use crate::device;
use crate::ui;
use rand::Rng;
const SI_DRAM_ADDR_REG: u32 = 0;
const SI_PIF_ADDR_RD64B_REG: u32 = 1;
@@ -38,7 +38,7 @@ pub fn read_regs(
device.si.regs[((address & 0xFFFF) >> 2) as usize]
}
fn randomize_interrupt_time(rng: &mut rand_chacha::ChaCha8Rng) -> u64 {
fn randomize_interrupt_time(rng: &mut rand::rngs::Xoshiro256PlusPlus) -> u64 {
rng.next_u64() % 0x100
}
@@ -98,6 +98,8 @@ fn copy_pif_rdram(device: &mut device::Device) {
i += 4;
}
} else if device.si.dma_dir == DmaDir::Read {
// check RDP before SI writes to RDRAM
ui::video::check_framebuffers(dram_addr as u32, device::pif::PIF_RAM_SIZE as u32);
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
let data = u32::from_be_bytes(device.pif.ram[i..i + 4].try_into().unwrap());
+69 -56
View File
@@ -1,5 +1,4 @@
use crate::{cheats, device, netplay, ui};
use governor::clock::Clock;
use crate::{cheats, device, netplay, retroachievements, ui};
const VI_STATUS_REG: u32 = 0;
const VI_ORIGIN_REG: u32 = 1;
@@ -24,12 +23,10 @@ pub struct Vi {
pub delay: u64,
pub field: u32,
#[serde(skip)]
pub limiter: Option<governor::DefaultDirectRateLimiter>,
pub next_pace_deadline: Option<std::time::Instant>,
pub count_per_scanline: u64,
pub enable_speed_limiter: bool,
pub vi_counter: u64,
pub last_origin: u32,
pub internal_frame_counter: u64,
pub min_wait_time: std::time::Duration,
pub frame_time: f64,
pub limit_freq: u64,
@@ -50,17 +47,11 @@ pub fn set_expected_refresh_rate(device: &mut device::Device) {
device.vi.count_per_scanline =
device.vi.delay / (device.vi.regs[VI_V_SYNC_REG as usize] + 1) as u64;
create_limiter(device);
reset_pace_deadline(device);
}
fn create_limiter(device: &mut device::Device) {
let quota = governor::Quota::with_period(std::time::Duration::from_secs_f64(
device.vi.frame_time * device.vi.limit_freq as f64,
))
.unwrap();
device.vi.limiter = Some(governor::RateLimiter::direct(quota));
let _ = device.vi.limiter.as_ref().unwrap().check();
//println!("new limit freq: {}", device.vi.limit_freq);
fn reset_pace_deadline(device: &mut device::Device) {
device.vi.next_pace_deadline = None;
}
fn set_vertical_interrupt(device: &mut device::Device) {
@@ -70,13 +61,10 @@ fn set_vertical_interrupt(device: &mut device::Device) {
}
fn set_current_line(device: &mut device::Device) {
let delay = device.vi.delay;
let next_vi = device::events::get_event(device, device::events::EVENT_TYPE_VI);
if let Some(next_vi) = next_vi {
device.vi.regs[VI_CURRENT_REG as usize] = ((delay
- (next_vi.count - device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG as usize]))
/ device.vi.count_per_scanline)
as u32;
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;
// 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] {
@@ -120,10 +108,10 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
}
VI_ORIGIN_REG => {
let current_origin = device.vi.regs[reg as usize];
device::memory::masked_write_32(&mut device.vi.regs[reg as usize], value, mask);
if device.vi.regs[reg as usize] != device.vi.last_origin {
device.vi.last_origin = device.vi.regs[reg as usize];
device.vi.internal_frame_counter += 1;
if current_origin != device.vi.regs[reg as usize] {
let _ = device.ui.video.fps_tx.try_send(true);
}
}
_ => {
@@ -137,29 +125,38 @@ pub fn vertical_interrupt_event(device: &mut device::Device) {
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
let mut speed_limiter_toggled = ui::video::check_callback(device);
if device.netplay.is_some() {
netplay::send_sync_check(device);
if device.vi.enable_speed_limiter == device.netplay.as_ref().unwrap().fast_forward {
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 = !device.netplay.as_ref().unwrap().fast_forward;
device.vi.enable_speed_limiter = !netplay.fast_forward;
}
}
device.vi.vi_counter += 1;
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();
/*
let vis = if device.cart.pal { 50 } else { 60 };
if device.vi.vi_counter % vis == 0 {
println!("FPS: {}", device.vi.internal_frame_counter);
device.vi.internal_frame_counter = 0;
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;
@@ -181,35 +178,51 @@ pub fn init(device: &mut device::Device) {
}
}
fn speed_limiter(device: &mut device::Device, speed_limiter_toggled: bool) {
let result = device.vi.limiter.as_ref().unwrap().check();
if let Err(outcome) = result {
let dur = outcome.wait_time_from(governor::clock::DefaultClock::default().now());
spin_sleep::sleep(dur);
if dur < device.vi.min_wait_time {
device.vi.min_wait_time = dur;
}
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);
let _ = device.vi.limiter.as_ref().unwrap().check();
} else {
device.vi.min_wait_time = std::time::Duration::from_secs(0);
let now = std::time::Instant::now();
match device.vi.next_pace_deadline {
None => {
device.vi.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;
}
} else {
//println!("did not sleep");
device.vi.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);
}
}
if std::time::Instant::now()
.duration_since(device.vi.limit_freq_check)
.as_secs_f64()
> 1.0
if std::time::Instant::now().duration_since(device.vi.limit_freq_check)
> std::time::Duration::from_secs(1)
{
if !speed_limiter_toggled {
if device.vi.min_wait_time.as_secs_f64() == 0.0 && device.vi.limit_freq < MAX_LIMIT_FREQ
if device.vi.min_wait_time == std::time::Duration::from_secs(0)
&& device.vi.limit_freq < MAX_LIMIT_FREQ
{
device.vi.limit_freq += 1;
create_limiter(device);
} else if device.vi.min_wait_time.as_secs_f64() > device.vi.frame_time
reset_pace_deadline(device);
} else if device.vi.min_wait_time
> std::time::Duration::from_secs_f64(device.vi.frame_time)
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
reset_pace_deadline(device);
}
}
+221 -98
View File
@@ -6,172 +6,295 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
use ui::gui;
use std::io::Error;
/// N64 emulator
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[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(
short,
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)."
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
configure_input_profile: Option<String>,
#[arg(
short,
long,
help = "Use DirectInput when configuring a new input profile."
)]
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
use_dinput: bool,
#[arg(
short,
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(
short,
long,
help = "Lists connected controllers which can be used in --assign-controller"
)]
list_controllers: bool,
#[arg(
short,
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(
short,
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
port: Option<usize>,
#[arg(
short = 'z',
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]
async fn main() {
#[tokio::main(worker_threads = 4)]
async fn main() -> std::io::Result<()> {
let dirs = ui::get_dirs();
let mut result = std::fs::create_dir_all(dirs.config_dir);
if result.is_err() {
panic!("could not create config dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.data_dir.join("saves"));
if result.is_err() {
panic!("could not create save dir: {}", result.err().unwrap())
}
result = std::fs::create_dir_all(dirs.data_dir.join("states"));
if result.is_err() {
panic!("could not create state dir: {}", result.err().unwrap())
}
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();
let args_as_strings: Vec<String> = std::env::args().collect();
let args_count = args_as_strings.len();
if args_count > 1 && args.game.is_none() {
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;
return Ok(());
}
if args.port.is_some() {
let port = args.port.unwrap();
if !(1..=4).contains(&port) {
println!("Port must be betwen 1 and 4");
return;
}
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;
return Ok(());
}
if args.configure_input_profile.is_some() {
if let Some(profile) = args.configure_input_profile {
ui::input::configure_input_profile(
&mut ui,
args.configure_input_profile.unwrap(),
profile,
args.use_dinput,
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
);
return;
return Ok(());
}
if args.assign_controller.is_some() {
if args.port.is_none() {
println!("Must specify port number");
return;
}
ui::input::assign_controller(
&mut ui,
args.assign_controller.unwrap(),
args.port.unwrap(),
);
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 args.bind_input_profile.is_some() {
if args.port.is_none() {
println!("Must specify port number");
return;
}
ui::input::bind_input_profile(
&mut ui,
args.bind_input_profile.unwrap(),
args.port.unwrap(),
);
}
} else if args.game.is_some() {
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
if let Some(rom_contents) = device::get_rom_contents(file_path) {
let handle = std::thread::Builder::new()
.name("n64".to_string())
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
.spawn(move || {
let mut device = device::Device::new();
let overclock = device.ui.config.emulation.overclock;
let disable_expansion_pak = device.ui.config.emulation.disable_expansion_pak;
let game_cheats = {
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
};
device::run_game(
&mut device,
rom_contents,
ui::gui::GameSettings {
fullscreen: args.fullscreen,
overclock,
disable_expansion_pak,
cheats: game_cheats,
},
);
})
.unwrap();
handle.join().unwrap();
} else {
println!("Could not read rom file");
return;
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 {
gui::app_window();
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
retroachievements::shutdown_client();
}
}
Ok(())
}
+11 -20
View File
@@ -46,14 +46,6 @@ struct InputEvent {
plugin: u8,
}
pub const NETPLAY_ERROR_DESYNC: u32 = 0;
pub const NETPLAY_ERROR_LOST_CONNECTION: u32 = 1;
pub const NETPLAY_ERROR_PLAYER_DISCONNECTED: u32 = 2;
pub const NETPLAY_ERROR_PLAYER_1_DISCONNECTED: u32 = 3;
pub const NETPLAY_ERROR_PLAYER_2_DISCONNECTED: u32 = 4;
pub const NETPLAY_ERROR_PLAYER_3_DISCONNECTED: u32 = 5;
pub const NETPLAY_ERROR_PLAYER_4_DISCONNECTED: u32 = 6;
pub fn send_rtc(netplay: &mut Netplay, rtc: i64) {
let mut request: Vec<u8> = [TCP_SEND_RTC].to_vec();
let size: u32 = 8;
@@ -93,7 +85,7 @@ pub fn send_save(netplay: &mut Netplay, save_type: &str, save_data: &[u8], size:
request.extend_from_slice(&(size as u32).to_be_bytes());
if size > 0 {
request.extend(save_data.to_owned());
request.extend_from_slice(save_data);
}
netplay.tcp_stream.write_all(&request).unwrap();
}
@@ -111,14 +103,13 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
*save_data = response;
}
pub fn send_sync_check(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
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 i in 0..device::cop0::COP0_REGS_COUNT as usize {
request.extend_from_slice(&(device.cpu.cop0.regs[i] as u32).to_be_bytes());
for item in regs.iter() {
request.extend_from_slice(&(*item as u32).to_be_bytes());
}
netplay.udp_socket.send(&request).unwrap();
@@ -144,7 +135,7 @@ pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::Inpu
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, &device.ui); // we execute process_incoming before request_input so that we send an accurate buffer count
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);
@@ -155,7 +146,7 @@ pub fn get_input(device: &mut device::Device, channel: usize) -> ui::input::Inpu
.remove(&netplay.player_data[channel].count);
if std::time::Instant::now() > timeout {
ui::audio::play_netplay_audio(&device.ui, NETPLAY_ERROR_LOST_CONNECTION);
ui::video::onscreen_message("Lost connection to netplay server", false);
input = Some(InputEvent {
input: 0,
plugin: 0,
@@ -186,7 +177,7 @@ fn request_input(netplay: &Netplay, channel: usize) {
netplay.udp_socket.send(&request).unwrap();
}
fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
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] {
@@ -200,14 +191,14 @@ fn process_incoming(netplay: &mut Netplay, ui: &ui::Ui) {
}
if current_status != netplay.status {
if ((current_status & 0x1) ^ (netplay.status & 0x1)) != 0 {
ui::audio::play_netplay_audio(ui, NETPLAY_ERROR_DESYNC);
ui::video::onscreen_message("Netplay desync detected", true);
}
for dis in 1..5 {
if ((current_status & (0x1 << dis)) ^ (netplay.status & (0x1 << dis))) != 0
{
ui::audio::play_netplay_audio(
ui,
NETPLAY_ERROR_PLAYER_DISCONNECTED + dis,
ui::video::onscreen_message(
&format!("Player {dis} disconnected"),
false,
);
}
}
+250
View File
@@ -0,0 +1,250 @@
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()
});
#[derive(serde::Serialize, serde::Deserialize)]
pub struct RAConfig {
pub username: String,
pub token: String,
pub enabled: bool,
pub hardcore: bool,
pub challenge: bool,
pub leaderboard: bool,
}
#[unsafe(no_mangle)]
pub extern "C" fn notify_load_game(ctx: *mut std::ffi::c_void) {
let tx = unsafe { Box::from_raw(ctx as *mut tokio::sync::oneshot::Sender<bool>) };
tx.send(true).unwrap();
}
#[unsafe(no_mangle)]
pub extern "C" fn store_retroachievements_credentials(
c_username: *const std::ffi::c_char,
c_token: *const std::ffi::c_char,
ctx: *mut std::ffi::c_void,
) {
let tx = unsafe { Box::from_raw(ctx as *mut tokio::sync::oneshot::Sender<bool>) };
if c_username.is_null() || c_token.is_null() {
tx.send(false).unwrap();
return;
}
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
&& let Ok(result) = serde_json::from_slice::<RAConfig>(ra_config.as_ref())
{
RAConfig {
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
enabled: result.enabled,
hardcore: result.hardcore,
challenge: result.challenge,
leaderboard: result.leaderboard,
}
} else {
RAConfig {
username: unsafe { std::ffi::CStr::from_ptr(c_username).to_str().unwrap() }.to_string(),
token: unsafe { std::ffi::CStr::from_ptr(c_token).to_str().unwrap() }.to_string(),
enabled: false,
hardcore: false,
challenge: false,
leaderboard: false,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
tx.send(true).unwrap();
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_server_call(
c_url: *const std::ffi::c_char,
c_post_data: *const std::ffi::c_char,
c_content_type: *const std::ffi::c_char,
c_callback: *mut std::ffi::c_void,
c_callback_data: *mut std::ffi::c_void,
) {
let url = unsafe { std::ffi::CStr::from_ptr(c_url).to_str().unwrap() }.to_string();
let task = if !c_post_data.is_null() {
let post_data =
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
.post(url)
.body(post_data)
.header(reqwest::header::CONTENT_TYPE, content_type)
.send()
} else {
WEB_CLIENT.get(url).send()
};
let callback = c_callback.addr();
let callback_data = c_callback_data.addr();
tokio::spawn(async move {
let response = task.await;
match response {
Ok(response) => {
let status = response.status().as_u16() as i32;
match response.text().await {
Ok(text) => {
let c_text = std::ffi::CString::new(text).unwrap();
unsafe {
ra_http_callback(
c_text.as_ptr(),
c_text.count_bytes(),
status,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
Err(error) => {
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
unsafe {
ra_http_callback(
c_error.as_ptr(),
c_error.count_bytes(),
status,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
}
}
Err(error) => {
let c_error = std::ffi::CString::new(error.to_string()).unwrap();
unsafe {
ra_http_callback(
c_error.as_ptr(),
c_error.count_bytes(),
0,
callback as *mut std::ffi::c_void,
callback_data as *mut std::ffi::c_void,
)
};
}
}
});
}
pub async fn load_game(rom: &[u8], rom_size: usize) {
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();
}
pub fn unload_game() {
unsafe { ra_unload_game() };
}
pub fn welcome() {
unsafe { ra_welcome() };
}
pub fn set_dmem(dmem: *const u8, dmem_size: usize) {
unsafe { ra_set_dmem(dmem, dmem_size) };
}
pub fn do_frame() {
unsafe { ra_do_frame() };
}
pub fn do_idle() {
unsafe { ra_do_idle() };
}
pub fn init_client(hardcore: bool, challenge: bool, leaderboard: bool) {
unsafe { ra_init_client(hardcore, challenge, leaderboard) };
}
pub fn shutdown_client() {
unsafe { ra_shutdown_client() };
}
pub fn get_hardcore() -> bool {
unsafe { ra_get_hardcore() }
}
pub fn login_user(username: String, password: String, tx: tokio::sync::oneshot::Sender<bool>) {
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
let c_username = std::ffi::CString::new(username).unwrap();
let c_password = std::ffi::CString::new(password).unwrap();
ra_login_user(c_username.as_ptr(), c_password.as_ptr(), tx_ptr)
};
}
#[cfg(feature = "gui")]
pub fn logout_user() {
unsafe { ra_logout_user() };
}
pub fn login_token_user(username: String, token: String, tx: tokio::sync::oneshot::Sender<bool>) {
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
let c_username = std::ffi::CString::new(username).unwrap();
let c_token = std::ffi::CString::new(token).unwrap();
ra_login_token_user(c_username.as_ptr(), c_token.as_ptr(), tx_ptr)
};
}
#[cfg(feature = "gui")]
pub fn get_username() -> Option<String> {
let c_username = unsafe { ra_get_username() };
if c_username.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(c_username) }
.to_str()
.unwrap()
.to_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() }
}
pub fn save_state(state: *mut u8, state_size: usize) {
unsafe { ra_save_state(state, state_size) };
}
pub fn load_state(state: *const u8, state_size: usize) {
unsafe { ra_load_state(state, state_size) };
}
+57 -24
View File
@@ -1,4 +1,4 @@
use crate::{device, ui};
use crate::{cheats, device, retroachievements, ui};
#[cfg(target_arch = "aarch64")]
use device::__m128i;
use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
@@ -20,7 +20,7 @@ impl<'de, const N: usize> Visitor<'de> for M128iArrayVisitor<N> {
A: SeqAccess<'de>,
{
let mut arr: [__m128i; N] = [device::zero_m128i(); N];
for (index, item) in arr.iter_mut().enumerate().take(N) {
for (index, item) in arr.iter_mut().enumerate() {
match seq.next_element::<u128>()? {
Some(value) => *item = unsafe { std::mem::transmute::<u128, __m128i>(value) },
None => return Err(serde::de::Error::invalid_length(index, &self)),
@@ -73,6 +73,9 @@ pub fn create_savestate(device: &device::Device) {
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"),
(
@@ -80,25 +83,33 @@ pub fn create_savestate(device: &device::Device) {
"saves",
),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
];
let save_path = device.ui.storage.paths.savestate_file_path.clone();
let compressed_file = ui::storage::compress_file(data);
std::fs::write(
device.ui.storage.paths.savestate_file_path.clone(),
compressed_file,
)
.unwrap();
println!(
"Savestate created at {}",
device.ui.storage.paths.savestate_file_path.display()
tokio::spawn(async move {
tokio::fs::write(save_path, compressed_file).await.unwrap();
});
ui::video::onscreen_message(
&format!(
"Savestate created in slot {}",
device.ui.storage.save_state_slot
),
false,
);
}
pub fn load_savestate(device: &mut device::Device) {
if retroachievements::get_hardcore() {
ui::video::onscreen_message("Cannot load savestate in RA hardcore mode", false);
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();
@@ -160,28 +171,22 @@ pub fn load_savestate(device: &mut device::Device) {
device::pif::connect_pif_channels(device);
for i in 0..4 {
if device.pif.channels[i].pak_handler.is_some() {
if device.pif.channels[i].pak_handler.unwrap().pak_type
== device::controller::PakType::RumblePak
{
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 device.pif.channels[i].pak_handler.unwrap().pak_type
== device::controller::PakType::MemPak
{
} 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 device.pif.channels[i].pak_handler.unwrap().pak_type
== device::controller::PakType::TransferPak
{
} 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,
@@ -195,13 +200,41 @@ pub fn load_savestate(device: &mut device::Device) {
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());
println!(
"Savestate loaded from {}",
device.ui.storage.paths.savestate_file_path.display()
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());
}
ui::video::onscreen_message(
&format!(
"Savestate loaded from slot {}",
device.ui.storage.save_state_slot
),
false,
);
} else {
println!("Failed to load savestate");
ui::video::onscreen_message(
&format!(
"Failed to load savestate from slot {}",
device.ui.storage.save_state_slot
),
false,
);
}
} else {
ui::video::onscreen_message(
&format!(
"Could not find savestate in slot {}",
device.ui.storage.save_state_slot
),
false,
);
}
}
+70 -43
View File
@@ -1,24 +1,28 @@
pub mod audio;
#[cfg(feature = "gui")]
pub mod cheats;
pub mod config;
#[cfg(feature = "gui")]
pub mod gui;
pub mod input;
#[cfg(feature = "gui")]
pub mod netplay;
#[cfg(feature = "gui")]
pub mod retroachievements;
pub mod storage;
pub mod usb;
pub mod video;
#[cfg(feature = "gui")]
pub mod vru;
pub struct Dirs {
pub config_dir: std::path::PathBuf,
pub data_dir: std::path::PathBuf,
pub cache_dir: std::path::PathBuf,
}
pub struct Audio {
pub audio_device_spec: sdl3_sys::audio::SDL_AudioSpec,
pub audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub event_audio_stream: *mut sdl3_sys::audio::SDL_AudioStream,
pub audio_device: u32,
pub event_audio: audio::EventAudio,
pub gain: f32,
}
@@ -32,11 +36,29 @@ pub struct Storage {
pub save_type: Vec<storage::SaveTypes>,
pub paths: storage::Paths,
pub saves: storage::Saves,
pub save_state_slot: u32,
}
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 struct Usb {
pub usb_tx: Option<tokio::sync::broadcast::Sender<usb::UsbData>>,
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
}
#[derive(Clone)]
pub struct GameSettings {
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub load_savestate_slot: Option<u32>,
}
pub struct Ui {
@@ -49,12 +71,14 @@ pub struct Ui {
pub input: Input,
pub storage: Storage,
pub video: Video,
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();
}
}
@@ -63,12 +87,23 @@ impl Drop for Ui {
pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
unsafe {
let init = sdl3_sys::init::SDL_WasInit(0);
if init & flag == 0 && !sdl3_sys::init::SDL_InitSubSystem(flag) {
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();
panic!("Could not initialize SDL subsystem: {flag}, {err}");
panic!(
"Could not initialize SDL subsystem: {}, {}",
u32::from(flag),
err
);
}
}
}
pub fn ttf_init() {
unsafe {
if !sdl3_ttf_sys::ttf::TTF_Init() {
panic!("Could not initialize TTF");
}
}
}
@@ -77,26 +112,27 @@ 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();
let config_dir;
let data_dir;
if portable {
config_dir = portable_dir.unwrap().join("portable_data").join("config");
data_dir = portable_dir.unwrap().join("portable_data").join("data");
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 {
config_dir = dirs::config_dir().unwrap().join("gopher64");
data_dir = dirs::data_dir().unwrap().join("gopher64");
};
Dirs {
config_dir,
data_dir,
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<u32>, with_sdl: bool) -> Ui {
fn construct_ui(joysticks: Vec<sdl3_sys::everything::SDL_JoystickID>, with_sdl: bool) -> 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);
Ui {
input: Input {
controllers: [
@@ -125,6 +161,7 @@ impl Ui {
joysticks,
},
storage: Storage {
save_state_slot: 0,
save_type: vec![],
paths: storage::Paths {
eep_file_path: std::path::PathBuf::new(),
@@ -139,27 +176,27 @@ impl Ui {
write_to_disk: true,
eeprom: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
sram: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
flash: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
mempak: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
sdcard: storage::Save {
data: Vec::new(),
written: false,
write_pending: false,
},
romsave: storage::RomSave {
data: std::collections::HashMap::new(),
written: false,
write_pending: false,
},
},
},
@@ -167,30 +204,20 @@ impl Ui {
game_id: String::new(),
game_hash: String::new(),
audio: Audio {
event_audio: audio::EventAudio {
mempak: include_bytes!("../data/mempak.wav").to_vec(),
rumblepak: include_bytes!("../data/rumblepak.wav").to_vec(),
transferpak: include_bytes!("../data/transferpak.wav").to_vec(),
netplay_desync: include_bytes!("../data/netplay_desync.wav").to_vec(),
netplay_lost_connection: include_bytes!("../data/netplay_lost_connection.wav")
.to_vec(),
netplay_disconnected: [
include_bytes!("../data/netplay_p1_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p2_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p3_disconnected.wav").to_vec(),
include_bytes!("../data/netplay_p4_disconnected.wav").to_vec(),
],
cheats_enabled: include_bytes!("../data/cheats_enabled.wav").to_vec(),
},
audio_device_spec: Default::default(),
audio_stream: std::ptr::null_mut(),
event_audio_stream: std::ptr::null_mut(),
audio_device: 0,
gain: 1.0,
},
video: Video {
window: std::ptr::null_mut(),
fullscreen: false,
fps_tx,
fps_rx: Some(fps_rx),
vis_tx,
vis_rx: Some(vis_rx),
},
usb: Usb {
usb_tx: None,
cart_rx: None,
},
dirs,
with_sdl,
+55 -141
View File
@@ -1,63 +1,8 @@
use crate::device;
use crate::netplay;
use crate::ui;
pub struct EventAudio {
pub mempak: Vec<u8>,
pub rumblepak: Vec<u8>,
pub transferpak: Vec<u8>,
pub netplay_desync: Vec<u8>,
pub netplay_lost_connection: Vec<u8>,
pub netplay_disconnected: [Vec<u8>; 4],
pub cheats_enabled: Vec<u8>,
}
pub fn init(ui: &mut ui::Ui, frequency: u64) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 48000,
channels: 2,
};
ui.audio.audio_device = unsafe {
sdl3_sys::audio::SDL_OpenAudioDevice(
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&device_audio_spec,
)
};
if ui.audio.audio_device == 0 {
panic!("Could not open audio device");
}
if !unsafe {
sdl3_sys::audio::SDL_GetAudioDeviceFormat(
ui.audio.audio_device,
&mut ui.audio.audio_device_spec,
std::ptr::null_mut(),
)
} {
panic!("Could not get audio device format");
}
let wav_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 24000,
channels: 1,
};
ui.audio.event_audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&wav_audio_spec, &ui.audio.audio_device_spec)
};
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(
ui.audio.audio_device,
ui.audio.event_audio_stream,
)
} {
panic!("Could not bind audio stream");
}
init_game_audio(ui, frequency);
}
@@ -69,29 +14,26 @@ pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
};
ui.audio.audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &ui.audio.audio_device_spec)
sdl3_sys::audio::SDL_OpenAudioDeviceStream(
sdl3_sys::audio::SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&game_audio_spec,
None,
std::ptr::null_mut(),
)
};
if ui.audio.audio_stream.is_null() {
return;
panic!("Could not create audio stream");
}
if !unsafe {
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain)
&& sdl3_sys::audio::SDL_BindAudioStream(ui.audio.audio_device, ui.audio.audio_stream)
&& sdl3_sys::audio::SDL_ResumeAudioStreamDevice(ui.audio.audio_stream)
} {
panic!("Could not bind audio stream");
panic!("Could not resume audio stream");
}
}
pub fn close(ui: &mut ui::Ui) {
close_game_audio(ui);
unsafe {
if !ui.audio.event_audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.event_audio_stream);
ui.audio.event_audio_stream = std::ptr::null_mut();
}
sdl3_sys::audio::SDL_CloseAudioDevice(ui.audio.audio_device);
ui.audio.audio_device = 0;
}
}
pub fn close_game_audio(ui: &mut ui::Ui) {
@@ -110,8 +52,11 @@ pub fn lower_audio_volume(ui: &mut ui::Ui) {
ui.audio.gain = 0.0;
}
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
}
ui::video::onscreen_message(
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
false,
);
}
pub fn raise_audio_volume(ui: &mut ui::Ui) {
@@ -121,77 +66,31 @@ pub fn raise_audio_volume(ui: &mut ui::Ui) {
ui.audio.gain = 2.0;
}
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.audio_stream, ui.audio.gain);
sdl3_sys::audio::SDL_SetAudioStreamGain(ui.audio.event_audio_stream, ui.audio.gain);
}
ui::video::onscreen_message(
&format!("Audio volume: {:.0}%", ui.audio.gain * 100.0),
false,
);
}
pub fn play_cheat_event(ui: &ui::Ui) {
if ui.audio.event_audio_stream.is_null() {
return;
}
let sound = &ui.audio.event_audio.cheats_enabled;
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
}
}
pub fn play_netplay_audio(ui: &ui::Ui, error: u32) {
if ui.audio.event_audio_stream.is_null() {
return;
}
let sound = match error {
netplay::NETPLAY_ERROR_DESYNC => &ui.audio.event_audio.netplay_desync,
netplay::NETPLAY_ERROR_LOST_CONNECTION => &ui.audio.event_audio.netplay_lost_connection,
netplay::NETPLAY_ERROR_PLAYER_1_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[0]
}
netplay::NETPLAY_ERROR_PLAYER_2_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[1]
}
netplay::NETPLAY_ERROR_PLAYER_3_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[2]
}
netplay::NETPLAY_ERROR_PLAYER_4_DISCONNECTED => {
&ui.audio.event_audio.netplay_disconnected[3]
}
_ => panic!("Invalid netplay error"),
};
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
}
}
pub fn play_pak_switch(ui: &ui::Ui, pak: device::controller::PakType) {
if ui.audio.event_audio_stream.is_null() {
fn adjust_audio_frequency(device: &device::Device, frequency: f32) {
if !device.vi.enable_speed_limiter {
return;
}
let sound = match pak {
device::controller::PakType::RumblePak => &ui.audio.event_audio.rumblepak,
device::controller::PakType::MemPak => &ui.audio.event_audio.mempak,
device::controller::PakType::TransferPak => &ui.audio.event_audio.transferpak,
_ => panic!("Invalid pak type"),
};
if !unsafe {
sdl3_sys::audio::SDL_PutAudioStreamData(
ui.audio.event_audio_stream,
sound.as_ptr() as *const std::ffi::c_void,
sound.len() as i32,
)
} {
panic!("Could not play audio");
unsafe {
let current_ratio =
sdl3_sys::everything::SDL_GetAudioStreamFrequencyRatio(device.ui.audio.audio_stream);
sdl3_sys::everything::SDL_SetAudioStreamFrequencyRatio(
device.ui.audio.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)
);
*/
}
}
@@ -215,11 +114,11 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
let audio_queued =
unsafe { sdl3_sys::audio::SDL_GetAudioStreamQueued(device.ui.audio.audio_stream) } as f64;
let acceptable_latency = (device.ai.freq as f64 * 0.2) * 4.0;
let min_latency = (device.ai.freq as f64 * 0.02) * 4.0;
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 < min_latency {
let silence_buffer: Vec<u8> = vec![0; min_latency as usize & !3];
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,
@@ -229,17 +128,32 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
} {
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 < acceptable_latency
&& !unsafe {
if audio_queued < max_latency {
if !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");
}
{
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);
}
}
+7 -32
View File
@@ -1,18 +1,10 @@
use crate::cheats;
use crate::device;
use crate::ui;
use crate::ui::gui::AppWindow;
use crate::ui::gui::ErrorDialog;
use slint::ComponentHandle;
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
note: String,
pub data: Vec<String>,
pub options: Option<std::collections::BTreeMap<String, String>>,
}
pub type Cheats = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
pub fn cheats_window(app: &AppWindow) {
let weak = app.as_weak();
app.on_cheats_select_rom_clicked(move |rom_dir| {
@@ -38,7 +30,7 @@ pub fn cheats_window(app: &AppWindow) {
handle.set_cheat_game_name(game_name.into());
})
.unwrap();
let cheats: Cheats =
let cheats: cheats::Cheat =
serde_json::from_slice(include_bytes!("../../data/cheats.json")).unwrap();
if let Some(cheat) = cheats.get(&game_crc) {
let cheat = cheat.clone();
@@ -77,16 +69,7 @@ pub fn cheats_window(app: &AppWindow) {
options,
));
}
#[allow(clippy::type_complexity)]
let cheats_model: std::rc::Rc<
slint::VecModel<(
slint::SharedString,
bool,
slint::SharedString,
slint::ModelRc<(bool, slint::SharedString)>,
)>,
> = std::rc::Rc::new(cheats_vec);
handle.set_cheats(slint::ModelRc::from(cheats_model));
handle.set_cheats(slint::ModelRc::from(std::rc::Rc::new(cheats_vec)));
})
.unwrap();
} else {
@@ -137,19 +120,11 @@ pub fn cheats_window(app: &AppWindow) {
fn clear_cheats(weak: &slint::Weak<AppWindow>, clear_name: bool) {
weak.upgrade_in_event_loop(move |handle| {
let cheats_vec = slint::VecModel::default();
#[allow(clippy::type_complexity)]
let cheats_model: std::rc::Rc<
slint::VecModel<(
slint::SharedString,
bool,
slint::SharedString,
slint::ModelRc<(bool, slint::SharedString)>,
)>,
> = std::rc::Rc::new(cheats_vec);
handle.set_cheats(slint::ModelRc::from(cheats_model));
handle.set_cheats(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::default(),
)));
if clear_name {
handle.set_cheat_game_name("".into());
handle.set_cheat_game_name(String::new().into());
}
})
.unwrap();
+11
View File
@@ -29,6 +29,7 @@ pub struct InputProfile {
pub joystick_hat: [InputJoystickHat; ui::input::PROFILE_SIZE],
pub joystick_axis: [InputControllerAxis; ui::input::PROFILE_SIZE],
pub dinput: bool,
pub deadzone: i32,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -38,6 +39,8 @@ pub struct Input {
pub controller_assignment: [Option<String>; 4],
pub controller_enabled: [bool; 4],
pub transfer_pak: [bool; 4],
pub gb_rom_path: [String; 4],
pub gb_ram_path: [String; 4],
pub emulate_vru: bool,
}
@@ -47,6 +50,7 @@ pub struct Video {
pub integer_scaling: bool,
pub fullscreen: bool,
pub widescreen: bool,
pub vsync: bool,
pub crt: bool,
}
@@ -54,6 +58,7 @@ pub struct Video {
pub struct Emulation {
pub overclock: bool,
pub disable_expansion_pak: bool,
pub usb: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -68,6 +73,7 @@ pub struct Config {
pub video: Video,
pub emulation: Emulation,
pub rom_dir: std::path::PathBuf,
pub recent_roms: Vec<String>,
}
impl Drop for Cheats {
@@ -138,6 +144,8 @@ impl Config {
input_profiles,
controller_enabled: [true, false, false, false],
transfer_pak: [false, false, false, false],
gb_rom_path: [String::new(), String::new(), String::new(), String::new()],
gb_ram_path: [String::new(), String::new(), String::new(), String::new()],
emulate_vru: false,
},
video: Video {
@@ -145,13 +153,16 @@ impl Config {
integer_scaling: false,
fullscreen: false,
widescreen: false,
vsync: true,
crt: false,
},
emulation: Emulation {
overclock: false,
disable_expansion_pak: false,
usb: false,
},
rom_dir: std::path::PathBuf::new(),
recent_roms: Vec::new(),
}
}
}
+340 -237
View File
@@ -1,11 +1,13 @@
use crate::device;
use crate::netplay;
use crate::retroachievements;
use crate::ui;
use slint::Model;
use slint::winit_030::WinitWindowAccessor;
slint::include_modules!();
pub const N64_EXTENSIONS: [&str; 5] = ["n64", "v64", "z64", "7z", "zip"];
pub const N64_EXTENSIONS: [&str; 12] = [
"n64", "v64", "z64", "7z", "zip", "bin", "N64", "V64", "Z64", "7Z", "ZIP", "BIN",
];
#[derive(serde::Deserialize)]
struct GithubData {
@@ -17,22 +19,12 @@ pub struct NetplayDevice {
pub player_number: u8,
}
pub struct GbPaths {
pub rom: [Option<std::path::PathBuf>; 4],
pub ram: [Option<std::path::PathBuf>; 4],
}
pub struct VruChannel {
pub vru_window_notifier: Option<tokio::sync::mpsc::Sender<Option<Vec<String>>>>,
pub vru_word_receiver: Option<tokio::sync::mpsc::Receiver<String>>,
}
#[derive(Clone)]
pub struct GameSettings {
pub fullscreen: bool,
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub struct RASettings {
pub enabled: bool,
pub hardcore: bool,
pub challenge: bool,
pub leaderboard: bool,
}
fn check_latest_version(weak: slint::Weak<AppWindow>) {
@@ -62,12 +54,92 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
});
}
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let dirs = ui::get_dirs();
fn run_with_path(
weak: slint::Weak<AppWindow>,
path: std::path::PathBuf,
controller_paths: &[Option<String>],
) {
let controller_paths = controller_paths.to_owned();
let weak2 = weak.clone();
weak.upgrade_in_event_loop(move |handle| {
if handle.get_game_running() {
return;
}
save_settings(&handle, &controller_paths);
run_rom(
path,
ui::GameSettings {
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: std::collections::HashMap::new(), // will be filled in later
load_savestate_slot: None,
},
None,
RASettings {
enabled: handle.get_ra_enabled(),
hardcore: handle.get_ra_hardcore(),
challenge: handle.get_ra_challenge(),
leaderboard: handle.get_ra_leaderboard(),
},
weak2,
);
})
.unwrap();
}
fn file_dropped(app: &AppWindow, controller_paths: &[Option<String>]) {
let weak = app.as_weak();
let controller_paths = controller_paths.to_owned();
app.window()
.on_winit_window_event(move |_winit_window, event| {
if let slint::winit_030::winit::event::WindowEvent::DroppedFile(path) = event {
run_with_path(weak.clone(), path.to_path_buf(), &controller_paths);
}
slint::winit_030::EventResult::Propagate
});
}
fn local_game_window(
app: &AppWindow,
config: &ui::config::Config,
controller_paths: &[Option<String>],
) {
let dirs = ui::get_dirs();
app.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
config
.recent_roms
.iter()
.filter(|x| {
if let Ok(exists) = std::fs::exists(x)
&& exists
{
true
} else {
false
}
})
.map(|x| {
(
x.into(),
std::path::Path::new(x)
.file_name()
.unwrap()
.to_str()
.unwrap()
.into(),
)
})
.collect::<Vec<(slint::SharedString, slint::SharedString)>>(),
),
)));
let weak = app.as_weak();
let owned_controller_paths = controller_paths.to_owned();
app.on_open_rom_button_clicked(move || {
let controller_paths = controller_paths.clone();
let controller_paths = owned_controller_paths.clone();
weak.upgrade_in_event_loop(move |handle| {
save_settings(&handle, &controller_paths);
open_rom(&handle)
@@ -75,10 +147,25 @@ fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>]) {
.unwrap();
});
let weak = app.as_weak();
let owned_controller_paths = controller_paths.to_owned();
app.on_recent_rom_button_clicked(move |rom| {
let controller_paths = owned_controller_paths.clone();
weak.upgrade_in_event_loop(move |handle| {
run_with_path(
handle.as_weak(),
std::path::PathBuf::from(rom.to_string()),
&controller_paths,
);
})
.unwrap();
});
let saves_path = dirs.data_dir.join("saves");
app.on_saves_folder_button_clicked(move || {
open::that_detached(saves_path.clone()).unwrap();
});
file_dropped(app, controller_paths);
}
fn input_profiles(config: &ui::config::Config) -> Vec<String> {
@@ -93,13 +180,16 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
app.set_integer_scaling(config.video.integer_scaling);
app.set_fullscreen(config.video.fullscreen);
app.set_widescreen(config.video.widescreen);
app.set_vsync(config.video.vsync);
app.set_apply_crt_shader(config.video.crt);
app.set_overclock_n64_cpu(config.emulation.overclock);
app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
app.set_emulate_usb(config.emulation.usb);
let combobox_value = match config.video.upscale {
1 => 0,
2 => 1,
4 => 2,
8 => 3,
_ => 0,
};
app.set_resolution(combobox_value);
@@ -127,17 +217,17 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
profile_bindings.push(position.unwrap() as i32);
}
let input_profiles = slint::VecModel::default();
for profile in profiles {
input_profiles.push(profile.into());
}
let input_profiles_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(input_profiles);
handle.set_input_profiles(slint::ModelRc::from(input_profiles_model));
handle.set_input_profiles(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
profiles
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
let input_profile_binding_model: std::rc::Rc<slint::VecModel<i32>> =
std::rc::Rc::new(profile_bindings);
handle.set_selected_profile_binding(slint::ModelRc::from(input_profile_binding_model));
handle
.set_selected_profile_binding(slint::ModelRc::from(std::rc::Rc::new(profile_bindings)));
// this is a workaround to make the input profile combobox update
handle.set_blank_profiles(true);
@@ -155,51 +245,70 @@ fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Con
fn controller_window(
app: &AppWindow,
config: &ui::config::Config,
controller_names: &Vec<String>,
controller_names: &[String],
controller_paths: &[Option<String>],
) {
let controller_enabled_model: std::rc::Rc<slint::VecModel<bool>> = std::rc::Rc::new(
slint::VecModel::from(config.input.controller_enabled.to_vec()),
);
app.set_emulate_vru(config.input.emulate_vru);
app.set_controller_enabled(slint::ModelRc::from(controller_enabled_model));
app.set_controller_enabled(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(config.input.controller_enabled.to_vec()),
)));
let transferpak_enabled_model: std::rc::Rc<slint::VecModel<bool>> =
std::rc::Rc::new(slint::VecModel::from(config.input.transfer_pak.to_vec()));
app.set_transferpak(slint::ModelRc::from(transferpak_enabled_model));
app.set_transferpak(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(config.input.transfer_pak.to_vec()),
)));
app.set_gb_rom_paths(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
config
.input
.gb_rom_path
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
app.set_gb_ram_paths(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
config
.input
.gb_ram_path
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
update_input_profiles(&app.as_weak(), config);
let controllers = slint::VecModel::default();
for controller in controller_names {
controllers.push(controller.into());
}
let controller_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(controllers);
app.set_controller_names(slint::ModelRc::from(controller_names_model));
app.set_controller_names(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
controller_names
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
let controller_changed = slint::VecModel::default();
let selected_controllers = slint::VecModel::default();
for selected in config.input.controller_assignment.iter() {
let mut found = false;
for (i, path) in controller_paths.iter().enumerate() {
if selected == path {
selected_controllers.push(i as i32);
found = true;
continue;
}
}
if !found {
selected_controllers.push(0);
}
let selected_index = controller_paths
.iter()
.position(|path| selected == path)
.unwrap_or(0) as i32;
selected_controllers.push(selected_index);
controller_changed.push(false);
}
let selected_controllers_model: std::rc::Rc<slint::VecModel<i32>> =
std::rc::Rc::new(selected_controllers);
app.set_selected_controller(slint::ModelRc::from(selected_controllers_model));
app.set_selected_controller(slint::ModelRc::from(std::rc::Rc::new(selected_controllers)));
app.set_controller_changed(slint::ModelRc::from(std::rc::Rc::new(controller_changed)));
let weak_app = app.as_weak();
app.on_input_profile_button_clicked(move || {
let dialog = InputProfileDialog::new().unwrap();
dialog.set_deadzone(ui::input::DEADZONE_DEFAULT);
let weak_dialog = dialog.as_weak();
let weak_app = weak_app.clone();
dialog.on_profile_creation_button_clicked(move || {
@@ -207,19 +316,78 @@ fn controller_window(
weak_dialog
.upgrade_in_event_loop(move |handle| {
handle.hide().unwrap();
let profile_name = handle.get_profile_name().into();
let profile_name = handle.get_profile_name();
let dinput = handle.get_dinput();
let deadzone = handle.get_deadzone();
tokio::spawn(async move {
let mut game_ui = ui::Ui::new();
ui::input::configure_input_profile(&mut game_ui, profile_name, dinput);
update_input_profiles(&weak_app, &game_ui.config);
let mut command =
tokio::process::Command::new(std::env::current_exe().unwrap());
command.args([
"--configure-input-profile",
&profile_name,
"--deadzone",
&deadzone.to_string(),
]);
if dinput {
command.arg("--use-dinput");
}
if !command.status().await.unwrap().success() {
eprintln!("Failed to configure input profile");
}
let config = ui::config::Config::new();
update_input_profiles(&weak_app, &config);
});
})
.unwrap();
});
dialog.show().unwrap();
});
let weak_app2 = app.as_weak();
app.on_transferpak_toggled(move |player, enabled| {
if enabled {
let select_gb_rom = rfd::AsyncFileDialog::new()
.set_title(format!("GB ROM P{}", player + 1))
.add_filter("GB ROM files", &["gb", "gbc", "GB", "GBC"])
.pick_file();
let select_gb_ram = rfd::AsyncFileDialog::new()
.set_title(format!("GB RAM P{}", player + 1))
.add_filter("GB RAM files", &["sav", "ram", "srm", "SAV", "RAM", "SRM"])
.pick_file();
let weak_app3 = weak_app2.clone();
tokio::spawn(async move {
if let (Some(gb_rom), Some(gb_ram)) = (select_gb_rom.await, select_gb_ram.await) {
weak_app3
.upgrade_in_event_loop(move |handle| {
let rom_paths = handle.get_gb_rom_paths();
let ram_paths = handle.get_gb_ram_paths();
rom_paths.set_row_data(
player as usize,
gb_rom.path().to_str().unwrap().into(),
);
ram_paths.set_row_data(
player as usize,
gb_ram.path().to_str().unwrap().into(),
);
handle.set_gb_rom_paths(rom_paths);
handle.set_gb_ram_paths(ram_paths);
})
.unwrap();
} else {
weak_app3
.upgrade_in_event_loop(move |handle| {
let rom_paths = handle.get_gb_rom_paths();
let ram_paths = handle.get_gb_ram_paths();
rom_paths.set_row_data(player as usize, String::new().into());
ram_paths.set_row_data(player as usize, String::new().into());
handle.set_gb_rom_paths(rom_paths);
handle.set_gb_ram_paths(ram_paths);
})
.unwrap();
}
});
}
});
}
pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
@@ -227,10 +395,12 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
config.video.integer_scaling = app.get_integer_scaling();
config.video.fullscreen = app.get_fullscreen();
config.video.widescreen = app.get_widescreen();
config.video.vsync = app.get_vsync();
config.video.crt = app.get_apply_crt_shader();
config.emulation.overclock = app.get_overclock_n64_cpu();
config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
let upscale_values = [1, 2, 4];
config.emulation.usb = app.get_emulate_usb();
let upscale_values = [1, 2, 4, 8];
config.video.upscale = upscale_values[app.get_resolution() as usize];
config.input.emulate_vru = app.get_emulate_vru();
@@ -239,6 +409,8 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
}
for (i, transferpak_enabled) in app.get_transferpak().iter().enumerate() {
config.input.transfer_pak[i] = transferpak_enabled;
config.input.gb_rom_path[i] = app.get_gb_rom_paths().row_data(i).unwrap().to_string();
config.input.gb_ram_path[i] = app.get_gb_ram_paths().row_data(i).unwrap().to_string();
}
for (i, input_profile_binding) in app.get_selected_profile_binding().iter().enumerate() {
config.input.input_profile_binding[i] = app
@@ -249,8 +421,10 @@ pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
}
for (i, selected_controller) in app.get_selected_controller().iter().enumerate() {
config.input.controller_assignment[i] =
controller_paths[selected_controller as usize].clone();
if app.get_controller_changed().row_data(i).unwrap_or(false) {
config.input.controller_assignment[i] =
controller_paths[selected_controller as usize].clone();
}
}
}
@@ -273,13 +447,16 @@ fn about_window(app: &AppWindow) {
app.on_newversion_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
});
app.set_version(format!("Version: {}", env!("CARGO_PKG_VERSION")).into());
check_latest_version(app.as_weak());
app.set_version(format!("Version: {}", env!("GIT_DESCRIBE")).into());
if std::env::var("FLATPAK_ID").is_err() {
check_latest_version(app.as_weak());
}
}
pub fn app_window() {
let app = AppWindow::new().unwrap();
about_window(&app);
ui::retroachievements::ra_window(&app);
let mut controller_paths;
{
let game_ui = ui::Ui::new();
@@ -289,124 +466,103 @@ pub fn app_window() {
controller_paths.insert(0, None);
settings_window(&app, &game_ui.config);
controller_window(&app, &game_ui.config, &controller_names, &controller_paths);
local_game_window(&app, &game_ui.config, &controller_paths);
}
local_game_window(&app, &controller_paths);
ui::netplay::netplay_window(&app, &controller_paths);
ui::cheats::cheats_window(&app);
app.run().unwrap();
save_settings(&app, &controller_paths);
}
fn setup_vru_word_watcher(
weak_vru: slint::Weak<AppWindow>,
vru_word_notifier: tokio::sync::mpsc::Sender<String>,
mut vru_window_receiver: tokio::sync::mpsc::Receiver<Option<Vec<String>>>,
) {
tokio::spawn(async move {
loop {
let notifier = vru_word_notifier.clone();
let notifier_closed = vru_word_notifier.clone();
let result = vru_window_receiver.recv().await;
if let Some(Some(words)) = result {
weak_vru
.upgrade_in_event_loop(move |_handle| {
let vru_dialog = VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak();
vru_dialog.on_vru_button_clicked(move |chosen_word| {
notifier.try_send(chosen_word.to_string()).unwrap();
vru_dialog_weak.unwrap().window().hide().unwrap();
});
vru_dialog.window().on_close_requested(move || {
notifier_closed.try_send("".to_string()).unwrap();
slint::CloseRequestResponse::HideWindow
});
let words_vec = slint::VecModel::default();
for word in words {
words_vec.push(word.into());
}
let words_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(words_vec);
vru_dialog.set_words(slint::ModelRc::from(words_model));
vru_dialog.show().unwrap();
})
.unwrap();
} else {
return;
}
}
});
}
pub fn run_rom(
gb_paths: GbPaths,
file_path: std::path::PathBuf,
mut game_settings: GameSettings,
vru_channel: VruChannel,
game_settings: ui::GameSettings,
netplay: Option<NetplayDevice>,
ra_settings: RASettings,
weak: slint::Weak<AppWindow>,
) {
std::thread::Builder::new()
.name("n64".to_string())
.stack_size(env!("N64_STACK_SIZE").parse().unwrap())
.spawn(move || {
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
.unwrap();
let mut device = device::Device::new();
device.ui.config.rom_dir = file_path.parent().unwrap().to_path_buf();
for i in 0..4 {
if gb_paths.rom[i].is_some() && gb_paths.ram[i].is_some() {
device.transferpaks[i].cart.rom =
std::fs::read(gb_paths.rom[i].as_ref().unwrap()).unwrap();
device.transferpaks[i].cart.ram =
std::fs::read(gb_paths.ram[i].as_ref().unwrap()).unwrap();
}
}
device.vru_window.window_notifier = vru_channel.vru_window_notifier;
device.vru_window.word_receiver = vru_channel.vru_word_receiver;
if let Some(rom_contents) = device::get_rom_contents(file_path.as_path()) {
if let Some(netplay_device) = netplay {
device.netplay = Some(netplay::init(
netplay_device.peer_addr,
netplay_device.player_number,
));
} else {
game_settings.cheats = {
let game_crc = ui::storage::get_game_crc(&rom_contents);
let cheats = ui::config::Cheats::new();
cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
};
}
device::run_game(&mut device, rom_contents, game_settings);
if device.netplay.is_some() {
netplay::close(&mut device);
}
} else {
println!("Could not read rom file");
}
if let Some(vru_window_notifier) = device.vru_window.window_notifier {
vru_window_notifier.try_send(None).unwrap();
}
let rom_dir = device.ui.config.rom_dir.clone();
weak.upgrade_in_event_loop(move |handle| {
if let Some(rom_dir_str) = rom_dir.to_str() {
handle.set_rom_dir(rom_dir_str.into());
}
handle.set_game_running(false);
})
tokio::spawn(async move {
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
.unwrap();
let mut command = tokio::process::Command::new(std::env::current_exe().unwrap());
command.args([
"--overclock",
&game_settings.overclock.to_string(),
"--disable-expansion-pak",
&game_settings.disable_expansion_pak.to_string(),
]);
let cheats_path = ui::get_dirs().cache_dir.join("cheats.json");
if let Some(netplay_device) = netplay {
let f = std::fs::File::create(cheats_path.to_str().unwrap()).unwrap();
serde_json::to_writer_pretty(f, &game_settings.cheats).unwrap();
command.args([
"--netplay-peer-addr",
&netplay_device.peer_addr.to_string(),
"--netplay-player-number",
&netplay_device.player_number.to_string(),
"--cheats",
cheats_path.to_str().unwrap(),
]);
}
if ra_settings.enabled {
command.args([
"--ra-username",
&retroachievements::get_username().unwrap_or("unknown".into()),
"--ra-token",
&retroachievements::get_token().unwrap_or("unknown".into()),
]);
if ra_settings.hardcore {
command.args(["--ra-hardcore"]);
}
if ra_settings.challenge {
command.args(["--ra-challenge"]);
}
if ra_settings.leaderboard {
command.args(["--ra-leaderboard"]);
}
}
let success = command
.arg(file_path.to_str().unwrap())
.status()
.await
.unwrap()
.success();
if !success {
eprintln!("Failed to run game");
}
let _ = std::fs::remove_file(cheats_path.to_str().unwrap());
weak.upgrade_in_event_loop(move |handle| {
if let Some(rom_dir) = file_path.parent().unwrap().to_str() {
handle.set_rom_dir(rom_dir.into());
}
if success {
let recent_roms = slint::VecModel::default();
recent_roms.push((
file_path.to_str().unwrap().into(),
file_path.file_name().unwrap().to_str().unwrap().into(),
));
for rom in handle.get_recent_roms().iter() {
if rom.0 != file_path.to_str().unwrap()
&& recent_roms.row_count() < 5
&& let Ok(exists) = std::fs::exists(&rom.0)
&& exists
{
recent_roms.push(rom);
}
}
handle.set_recent_roms(slint::ModelRc::from(std::rc::Rc::new(recent_roms)));
}
handle.set_game_running(false);
})
.unwrap();
});
}
fn open_rom(app: &AppWindow) {
@@ -422,85 +578,32 @@ fn open_rom(app: &AppWindow) {
.set_title("Select ROM")
.add_filter("ROM files", &N64_EXTENSIONS)
.pick_file();
let mut select_gb_rom = [None, None, None, None];
let mut select_gb_ram = [None, None, None, None];
for (i, transfer_pak_enabled) in app.get_transferpak().iter().enumerate() {
if transfer_pak_enabled {
select_gb_rom[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB ROM P{}", i + 1))
.add_filter("GB ROM files", &["gb", "gbc"])
.pick_file(),
);
select_gb_ram[i] = Some(
rfd::AsyncFileDialog::new()
.set_title(format!("GB RAM P{}", i + 1))
.add_filter("GB RAM files", &["sav", "ram"])
.pick_file(),
);
}
}
#[allow(clippy::type_complexity)]
let (vru_window_notifier, vru_window_receiver): (
tokio::sync::mpsc::Sender<Option<Vec<String>>>,
tokio::sync::mpsc::Receiver<Option<Vec<String>>>,
) = tokio::sync::mpsc::channel(5);
let (vru_word_notifier, vru_word_receiver): (
tokio::sync::mpsc::Sender<String>,
tokio::sync::mpsc::Receiver<String>,
) = tokio::sync::mpsc::channel(5);
let fullscreen = app.get_fullscreen();
let overclock = app.get_overclock_n64_cpu();
let emulate_vru = app.get_emulate_vru();
let disable_expansion_pak = app.get_disable_expansion_pak();
let ra_enabled = app.get_ra_enabled();
let ra_hardcore = app.get_ra_hardcore();
let ra_challenge = app.get_ra_challenge();
let ra_leaderboard = app.get_ra_leaderboard();
if emulate_vru {
setup_vru_word_watcher(app.as_weak(), vru_word_notifier, vru_window_receiver);
}
let weak = app.as_weak();
tokio::spawn(async move {
if let Some(file) = select_rom.await {
let mut gb_rom_path = [None, None, None, None];
let mut gb_ram_path = [None, None, None, None];
for i in 0..4 {
if let (Some(gb_rom), Some(gb_ram)) =
(select_gb_rom[i].as_mut(), select_gb_ram[i].as_mut())
&& let (Some(gb_rom), Some(gb_ram)) = (gb_rom.await, gb_ram.await)
{
gb_rom_path[i] = Some(gb_rom.path().to_path_buf());
gb_ram_path[i] = Some(gb_ram.path().to_path_buf());
}
}
run_rom(
GbPaths {
rom: gb_rom_path,
ram: gb_ram_path,
},
file.path().to_path_buf(),
GameSettings {
fullscreen,
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats: std::collections::HashMap::new(), // will be filled in later
},
if emulate_vru {
VruChannel {
vru_window_notifier: Some(vru_window_notifier),
vru_word_receiver: Some(vru_word_receiver),
}
} else {
VruChannel {
vru_window_notifier: None,
vru_word_receiver: None,
}
load_savestate_slot: None,
},
None,
RASettings {
enabled: ra_enabled,
hardcore: ra_hardcore,
challenge: ra_challenge,
leaderboard: ra_leaderboard,
},
weak,
);
}
+48 -3
View File
@@ -2,9 +2,10 @@
// SPDX-License-Identifier: MIT
import { StandardButton } from "std-widgets.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, Settings, About } from "pages.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, RetroAchievements, Settings, About } from "pages.slint";
import { SideBar } from "side_bar.slint";
import { SettingsData } from "settings_page.slint";
import { RAData } from "retroachievements_page.slint";
import { AboutData } from "about_page.slint";
import { ControllerData } from "controller_page.slint";
import { State } from "localgame_page.slint";
@@ -15,17 +16,20 @@ export { NetplayCreate } from "netplay_create.slint";
export { NetplayJoin } from "netplay_join.slint";
export { NetplayWait } from "netplay_wait.slint";
export { CustomNetplayServer } from "netplay_page.slint";
export { DispatcherDialog } from "netplay_create.slint";
export component AppWindow inherits Window {
callback wiki_button_clicked;
callback discord_button_clicked;
callback newversion_button_clicked;
callback recent_rom_button_clicked(string);
callback patreon_button_clicked;
callback github_sponsors_button_clicked;
callback source_code_button_clicked;
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback input_profile_button_clicked;
callback transferpak_toggled(int,bool);
callback create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
@@ -33,24 +37,44 @@ export component AppWindow inherits Window {
callback cheats_select_rom_clicked(string);
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
callback ra_button_clicked(string);
callback ra_games_clicked;
callback ra_show_profile_clicked;
callback ra_toggled(bool,bool,bool,bool);
in property version <=> AboutData.version;
in property has_update <=> AboutData.has_update;
in-out property ra_username <=> RAData.ra_username;
in-out property ra_password <=> RAData.ra_password;
in-out property ra_logging_in <=> RAData.ra_logging_in;
in-out property ra_show_profile <=> RAData.ra_show_profile;
in-out property ra_current_user_message <=> RAData.current_user_message;
in-out property ra_enabled <=> RAData.enabled;
in-out property ra_hardcore <=> RAData.hardcore;
in-out property ra_challenge <=> RAData.challenge;
in-out property ra_leaderboard <=> RAData.leaderboard;
in-out property ra_softcore_only <=> RAData.ra_softcore_only;
in-out property integer_scaling <=> SettingsData.integer_scaling;
in-out property fullscreen <=> SettingsData.fullscreen;
in-out property widescreen <=> SettingsData.widescreen;
in-out property vsync <=> SettingsData.vsync;
in-out property apply_crt_shader <=> SettingsData.apply_crt_shader;
in-out property overclock_n64_cpu <=> SettingsData.overclock_n64_cpu;
in-out property disable_expansion_pak <=> SettingsData.disable_expansion_pak;
in-out property resolution <=> SettingsData.resolution;
in-out property emulate_usb <=> SettingsData.emulate_usb;
in-out property rom_dir <=> SettingsData.rom_dir;
in-out property emulate_vru <=> ControllerData.emulate_vru;
in-out property controller_enabled <=> ControllerData.controller_enabled;
in-out property controller_changed <=> ControllerData.controller_changed;
in-out property transferpak <=> ControllerData.transferpak;
in-out property gb_rom_paths <=> ControllerData.gb_rom_paths;
in-out property gb_ram_paths <=> ControllerData.gb_ram_paths;
in-out property selected_profile_binding <=> ControllerData.selected_profile_binding;
in-out property input_profiles <=> ControllerData.input_profiles;
in-out property controller_names <=> ControllerData.controller_names;
in-out property selected_controller <=> ControllerData.selected_controller;
in-out property game_running <=> State.game_running;
in-out property recent_roms <=> State.recent_roms;
in-out property blank_profiles <=> ControllerData.blank_profiles;
in-out property cheat_game_name <=> CheatsData.cheat_game_name;
in-out property cheat_game_crc <=> CheatsData.cheat_game_crc;
@@ -68,6 +92,7 @@ export component AppWindow inherits Window {
@tr("Menu" => "Controller Config"),
@tr("Menu" => "Netplay"),
@tr("Menu" => "Cheats"),
@tr("Menu" => "RetroAchievements"),
@tr("Menu" => "Settings"),
@tr("Menu" => "About")
];
@@ -83,11 +108,17 @@ export component AppWindow inherits Window {
newversion_button_clicked => {
newversion_button_clicked();
}
recent_rom_button_clicked(rom) => {
recent_rom_button_clicked(rom);
}
}
if(side-bar.current-item == 1): ControllerConfig {
input_profile_button_clicked => {
input_profile_button_clicked();
}
transferpak_toggled(player,enabled) => {
transferpak_toggled(player,enabled);
}
}
if(side-bar.current-item == 2): Netplay {
create_session_button_clicked => {
@@ -114,8 +145,22 @@ export component AppWindow inherits Window {
cheat_toggled(game_crc,cheat_name,option,enabled);
}
}
if(side-bar.current-item == 4): Settings { }
if(side-bar.current-item == 5): About {
if(side-bar.current-item == 4): RetroAchievements {
ra_button_clicked(password) => {
ra_button_clicked(password);
}
ra_toggled(enabled,hardcore,challenge,leaderboard) => {
ra_toggled(enabled,hardcore,challenge,leaderboard);
}
ra_games_clicked => {
ra_games_clicked();
}
ra_show_profile_clicked => {
ra_show_profile_clicked();
}
}
if(side-bar.current-item == 5): Settings { }
if(side-bar.current-item == 6): About {
wiki_button_clicked => {
wiki_button_clicked();
}
+3 -4
View File
@@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, CheckBox } from "std-widgets.slint";
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, Switch, CheckBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { SettingsData } from "settings_page.slint";
@@ -62,15 +62,14 @@ export component Cheats inherits Page {
wrap: word-wrap;
}
if data.options.length == 0: CheckBox {
text: @tr("Enabled");
if data.options.length == 0: Switch {
checked: data.enabled;
toggled => {
data.enabled = self.checked;
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,"",self.checked);
}
}
for item[index] in data.options: CheckBox {
for item in data.options: CheckBox {
text: item.option;
enabled: !data.enabled || (data.enabled && item.enabled);
checked: item.enabled;
+14 -5
View File
@@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { CheckBox, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { Switch, ComboBox, Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { State } from "localgame_page.slint";
@@ -9,15 +9,19 @@ export global ControllerData {
in-out property <bool> emulate_vru;
in-out property <[bool]> controller_enabled;
in-out property <[bool]> transferpak;
in-out property <[string]> gb_rom_paths;
in-out property <[string]> gb_ram_paths;
in-out property <[int]> selected_profile_binding: [-1, -1, -1, -1];
in-out property <[string]> input_profiles;
in-out property <[string]> controller_names;
in-out property <[int]> selected_controller;
in-out property <[bool]> controller_changed;
in-out property <bool> blank_profiles;
}
export component ControllerConfig inherits Page {
callback input_profile_button_clicked;
callback transferpak_toggled(int,bool);
title: @tr("Controller Configuration");
VerticalBox {
@@ -51,7 +55,7 @@ export component ControllerConfig inherits Page {
title: @tr("Enabled");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
for player in [0, 1, 2, 3]: Switch {
checked: ControllerData.controller_enabled[player];
toggled => {
ControllerData.controller_enabled[player] = self.checked;
@@ -64,7 +68,7 @@ export component ControllerConfig inherits Page {
title: @tr("Emulate VRU");
VerticalLayout {
alignment: space-between;
for valid in [false, false, false, true]: CheckBox {
for valid in [false, false, false, true]: Switch {
enabled: valid;
checked: valid && ControllerData.emulate_vru;
toggled => {
@@ -78,10 +82,11 @@ export component ControllerConfig inherits Page {
title: @tr("Transfer Pak");
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: CheckBox {
for player in [0, 1, 2, 3]: Switch {
checked: ControllerData.transferpak[player];
toggled => {
ControllerData.transferpak[player] = self.checked;
transferpak_toggled(player,self.checked);
}
}
}
@@ -89,10 +94,12 @@ export component ControllerConfig inherits Page {
if !ControllerData.blank_profiles: GroupBox {
title: @tr("Profile");
horizontal-stretch: 0.25;
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.input_profiles;
min-width: self.preferred-width > 150px ? self.preferred-width : 150px;
current-index: ControllerData.selected_profile_binding[player];
selected => {
ControllerData.selected_profile_binding[player] = self.current-index;
@@ -103,14 +110,16 @@ export component ControllerConfig inherits Page {
GroupBox {
title: @tr("Controller");
horizontal-stretch: 0.75;
VerticalLayout {
alignment: space-between;
for player in [0, 1, 2, 3]: ComboBox {
model: ControllerData.controller_names;
width: self.preferred-width;
min-width: self.preferred-width > 300px ? self.preferred-width : 300px;
current-index: ControllerData.selected_controller[player];
selected => {
ControllerData.selected_controller[player] = self.current-index;
ControllerData.controller_changed[player] = true;
}
}
}
+26 -2
View File
@@ -1,8 +1,9 @@
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, CheckBox } from "std-widgets.slint";
import { StandardButton, HorizontalBox, VerticalBox, Button, LineEdit, Switch, Slider } from "std-widgets.slint";
export global InputProfileData {
in-out property <bool> dinput;
in-out property <string> profile_name;
in-out property <int> deadzone;
}
export component InputProfileDialog inherits Window {
@@ -10,6 +11,7 @@ export component InputProfileDialog inherits Window {
callback profile_creation_button_clicked;
in-out property dinput <=> InputProfileData.dinput;
in-out property profile_name <=> InputProfileData.profile_name;
in-out property deadzone <=> InputProfileData.deadzone;
VerticalBox {
HorizontalBox {
LineEdit {
@@ -23,7 +25,29 @@ export component InputProfileDialog inherits Window {
HorizontalBox {
alignment: center;
CheckBox {
Text {
text: @tr("Deadzone Percentage");
vertical-alignment: center;
}
Slider {
min-width: 160px;
maximum: 99;
value: InputProfileData.deadzone;
changed(value) => {
InputProfileData.deadzone = value;
}
}
Text {
text: InputProfileData.deadzone + "%";
vertical-alignment: center;
}
}
HorizontalBox {
alignment: center;
Switch {
text: @tr("Use DirectInput");
checked: InputProfileData.dinput;
toggled => {
+20 -1
View File
@@ -1,18 +1,20 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Button, HorizontalBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { AboutData, NewVersionButton } from "about_page.slint";
export global State {
in-out property <bool> game_running;
in-out property <[{a:string, b:string}]> recent_roms;
}
export component LocalGame inherits Page {
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback newversion_button_clicked;
callback recent_rom_button_clicked(string);
title: @tr("Local Game");
VerticalBox {
@@ -44,5 +46,22 @@ export component LocalGame inherits Page {
}
}
Rectangle { }
if State.recent_roms.length>0:HorizontalBox {
alignment: center;
GroupBox {
title: @tr("Recent ROMs");
VerticalLayout {
alignment: space-between;
for rom in State.recent_roms: Button {
text: rom.b;
enabled: !State.game_running;
clicked => {
recent_rom_button_clicked(rom.a);
}
}
}
}
}
}
}
+10 -12
View File
@@ -6,7 +6,6 @@ export component NetplayCreate inherits Window {
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property custom_server_url <=> NetplayData.custom_server_url;
in-out property ping <=> NetplayData.ping;
in-out property game_name <=> NetplayData.game_name;
in-out property game_hash <=> NetplayData.game_hash;
in-out property game_cheats <=> NetplayData.game_cheats;
@@ -14,7 +13,6 @@ export component NetplayCreate inherits Window {
in-out property rom_path <=> NetplayData.rom_path;
in-out property peer_addr <=> NetplayData.peer_addr;
in-out property rom_dir <=> NetplayData.rom_dir;
callback get_ping(string);
callback get_custom_url();
callback select_rom(string);
callback create_session(string, string, string, string, string, string, string);
@@ -27,7 +25,7 @@ export component NetplayCreate inherits Window {
}
LineEdit {
placeholder_text: @tr("Enter session name");
placeholder-text: @tr("Enter session name");
edited(text) => {
NetplayData.session_name = text;
}
@@ -40,7 +38,7 @@ export component NetplayCreate inherits Window {
}
LineEdit {
placeholder_text: @tr("Enter session password");
placeholder-text: @tr("Enter session password");
edited(text) => {
NetplayData.password = text;
}
@@ -54,7 +52,7 @@ export component NetplayCreate inherits Window {
}
LineEdit {
placeholder_text: @tr("Enter player name");
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
@@ -73,17 +71,10 @@ export component NetplayCreate inherits Window {
if (self.current-index >= 0) {
if (self.current-value == "Custom") {
get_custom_url();
} else {
get_ping(NetplayData.server_urls[self.current-index]);
}
}
}
}
Text {
vertical-alignment: center;
text: NetplayData.ping;
}
}
}
@@ -130,3 +121,10 @@ export component NetplayCreate inherits Window {
}
}
}
export component DispatcherDialog inherits Dialog {
title: @tr("Creating Server");
Text {
text: "Creating server, please wait...This may take about 30 seconds.";
}
}
+32 -35
View File
@@ -6,32 +6,32 @@ export component NetplayJoin inherits Window {
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property custom_server_url <=> NetplayData.custom_server_url;
in-out property ping <=> NetplayData.ping;
in-out property sessions <=> NetplayData.sessions;
in-out property current_session <=> NetplayData.current_session;
in-out property ports <=> NetplayData.ports;
in-out property room_urls <=> NetplayData.room_urls;
in-out property room_ports <=> NetplayData.room_ports;
in-out property game_name <=> NetplayData.game_name;
in-out property game_hash <=> NetplayData.game_hash;
in-out property game_cheats <=> NetplayData.game_cheats;
in-out property peer_addr <=> NetplayData.peer_addr;
in-out property pending_session <=> NetplayData.pending_session;
in-out property pending_refresh <=> NetplayData.pending_refresh;
in-out property rom_path <=> NetplayData.rom_path;
in-out property rom_dir <=> NetplayData.rom_dir;
callback get_ping(string);
callback get_custom_url();
callback refresh_session(string);
callback refresh_session();
callback select_rom(string);
callback join_session(string, string, string, int);
callback join_session(string, string, string, string, int);
preferred-width: 700px;
VerticalBox {
HorizontalBox {
VerticalBox {
Text {
text: @tr("Player Name:");
text: @tr("Player Name (required):");
}
LineEdit {
placeholder_text: @tr("Enter player name");
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
@@ -40,43 +40,37 @@ export component NetplayJoin inherits Window {
VerticalBox {
Text {
text: @tr("Server:");
text: @tr("Custom Server URL (optional):");
}
HorizontalBox {
server := ComboBox {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
if (self.current-value == "Custom") {
get_custom_url();
} else {
get_ping(NetplayData.server_urls[self.current-index]);
}
}
}
}
Text {
vertical-alignment: center;
text: NetplayData.ping;
LineEdit {
placeholder-text: @tr("example.com:45000");
text: NetplayData.custom_server_url;
edited(text) => {
NetplayData.custom_server_url = text;
}
}
}
}
if NetplayData.sessions.length > 0: Text {
if NetplayData.pending_refresh: Text {
text: @tr("Refreshing session list, please wait...");
horizontal-alignment: center;
vertical-alignment: center;
}
if !NetplayData.pending_refresh && NetplayData.sessions.length > 0: Text {
text: @tr("Click on a session below to select it");
horizontal-alignment: center;
vertical-alignment: center;
}
if NetplayData.sessions.length == 0: Text {
text: @tr("No sessions available. Please refresh or try a different server.");
if !NetplayData.pending_refresh && NetplayData.sessions.length == 0: Text {
text: @tr("No sessions available. Please refresh to check again.");
horizontal-alignment: center;
vertical-alignment: center;
}
StandardTableView {
columns: [
{ title: "Server" },
{ title: "Session Name" },
{ title: "Game Name" },
{ title: "Password" },
@@ -93,13 +87,15 @@ export component NetplayJoin inherits Window {
Rectangle { }
Button {
enabled: !NetplayData.pending_refresh;
text: @tr("Refresh Session List");
clicked => {
if (server.current-value == "Custom") {
refresh_session("ws://" + NetplayData.custom_server_url);
} else {
refresh_session(NetplayData.server_urls[server.current-index]);
}
NetplayData.pending_refresh = true;
NetplayData.current_session = -1;
NetplayData.sessions = [];
NetplayData.room_urls = [];
NetplayData.room_ports = [];
refresh_session();
}
}
}
@@ -134,7 +130,8 @@ export component NetplayJoin inherits Window {
NetplayData.player_name,
NetplayData.game_hash,
NetplayData.password,
NetplayData.ports[NetplayData.current_session]);
NetplayData.room_urls[NetplayData.current_session],
NetplayData.room_ports[NetplayData.current_session]);
}
}
}
+3 -2
View File
@@ -7,7 +7,6 @@ import { Page } from "page.slint";
export global NetplayData {
in-out property <[string]> server_names;
in-out property <[string]> server_urls;
in-out property <string> ping: "Ping: Unknown";
in-out property <string> player_name;
in-out property <string> game_name;
in-out property <string> game_hash;
@@ -16,9 +15,11 @@ export global NetplayData {
in-out property <string> password;
in-out property <string> peer_addr;
in-out property <bool> pending_session;
in-out property <bool> pending_refresh;
in-out property <string> session_name;
in-out property <[[StandardListViewItem]]> sessions;
in-out property <[int]> ports;
in-out property <[string]> room_urls;
in-out property <[int]> room_ports;
in-out property <int> current_session: -1;
in-out property <string> rom_dir;
in-out property <string> custom_server_url;
+8 -1
View File
@@ -7,6 +7,7 @@ export global NetplayWaitData {
in-out property <int> port;
in-out property <bool> can_start;
in-out property <string> chat_text;
in-out property <string> ping;
}
export component NetplayWait inherits Window {
@@ -21,6 +22,7 @@ export component NetplayWait inherits Window {
in-out property port <=> NetplayWaitData.port;
in-out property can_start <=> NetplayWaitData.can_start;
in-out property chat_text <=> NetplayWaitData.chat_text;
in-out property ping <=> NetplayWaitData.ping;
callback send_chat_message(string);
callback begin_game;
@@ -37,6 +39,11 @@ export component NetplayWait inherits Window {
horizontal-alignment: center;
}
Text {
text: @tr("Ping: ") + NetplayWaitData.ping;
horizontal-alignment: center;
}
GridBox {
Row {
Text {
@@ -66,7 +73,7 @@ export component NetplayWait inherits Window {
HorizontalBox {
chat_message := LineEdit {
placeholder_text: @tr("Enter chat message");
placeholder-text: @tr("Enter chat message");
key-released(event) => {
if (event.text == Key.Return && chat_message.text != "") {
send_chat_message(chat_message.text);
+1
View File
@@ -2,5 +2,6 @@ export { LocalGame } from "localgame_page.slint";
export { ControllerConfig } from "controller_page.slint";
export { Netplay } from "netplay_page.slint";
export { Cheats } from "cheat_page.slint";
export { RetroAchievements } from "retroachievements_page.slint";
export { Settings } from "settings_page.slint";
export { About } from "about_page.slint";
+143
View File
@@ -0,0 +1,143 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { LineEdit, Switch, Button, HorizontalBox, VerticalBox } from "std-widgets.slint";
import { Page } from "page.slint";
export global RAData {
in-out property <bool> enabled;
in-out property <bool> hardcore;
in-out property <bool> challenge;
in-out property <bool> leaderboard;
in-out property <bool> ra_logging_in;
in-out property <bool> ra_show_profile;
in-out property <bool> ra_softcore_only;
in-out property <string> ra_username;
in-out property <string> ra_password;
in-out property <string> current_user_message;
}
export component RetroAchievements inherits Page {
callback ra_button_clicked(string);
callback ra_toggled(bool,bool,bool,bool);
callback ra_games_clicked;
callback ra_show_profile_clicked;
title: @tr("RetroAchievements");
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
Switch {
text: @tr("Enabled");
checked: RAData.enabled;
toggled => {
RAData.enabled = self.checked;
if (!RAData.enabled) {
RAData.ra_username = "";
username_widget.text = "";
password_widget.text = "";
RAData.current_user_message = "Not currently logged in";
RAData.ra_show_profile = false;
}
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
if !RAData.ra_softcore_only:HorizontalBox {
alignment: start;
Switch {
text: @tr("Hardcore mode");
checked: RAData.hardcore;
enabled: RAData.enabled;
toggled => {
RAData.hardcore = self.checked;
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Challenge Indicators");
checked: RAData.challenge;
enabled: RAData.enabled;
toggled => {
RAData.challenge = self.checked;
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Leaderboard Trackers");
checked: RAData.leaderboard;
enabled: RAData.enabled;
toggled => {
RAData.leaderboard = self.checked;
ra_toggled(RAData.enabled,RAData.hardcore,RAData.challenge,RAData.leaderboard);
}
}
}
HorizontalBox {
alignment: center;
Text {
text: RAData.current_user_message;
vertical-alignment: center;
}
if RAData.ra_show_profile:Button {
enabled: RAData.enabled;
text: @tr("View Profile");
clicked => {
ra_show_profile_clicked();
}
}
}
HorizontalBox {
alignment: center;
username_widget := LineEdit {
enabled: RAData.enabled;
placeholder-text: @tr("Username");
text: RAData.ra_username;
edited(text) => {
RAData.ra_username = text;
}
}
password_widget := LineEdit {
enabled: RAData.enabled;
input-type: password;
placeholder-text: @tr("Password");
edited(text) => {
RAData.ra_password = text;
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Login");
enabled: RAData.enabled && !RAData.ra_logging_in && RAData.ra_username != "" && RAData.ra_password != "";
clicked => {
RAData.ra_logging_in = true;
ra_button_clicked(RAData.ra_password);
RAData.ra_password = "";
password_widget.text = "";
}
}
}
Rectangle {
height: 20px;
}
HorizontalBox {
alignment: center;
Button {
text: @tr("View Supported Games");
clicked => {
ra_games_clicked();
}
}
}
}
}
+34 -10
View File
@@ -1,17 +1,19 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ComboBox, HorizontalBox, VerticalBox, CheckBox } from "std-widgets.slint";
import { ComboBox, HorizontalBox, VerticalBox, Switch } from "std-widgets.slint";
import { Page } from "page.slint";
export global SettingsData {
in-out property <bool> integer_scaling;
in-out property <bool> fullscreen;
in-out property <bool> widescreen;
in-out property <bool> vsync;
in-out property <bool> apply_crt_shader;
in-out property <bool> overclock_n64_cpu;
in-out property <bool> disable_expansion_pak;
in-out property <int> resolution;
in-out property <bool> emulate_usb;
in-out property <string> rom_dir;
}
@@ -28,7 +30,7 @@ export component Settings inherits Page {
}
ComboBox {
model: ["1x", "2x", "4x"];
model: ["1x", "2x", "4x", "8x"];
current-index: SettingsData.resolution;
selected => {
SettingsData.resolution = self.current-index;
@@ -38,7 +40,7 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("Integer Scaling");
checked: SettingsData.integer_scaling;
toggled => {
@@ -49,8 +51,8 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Fullscreen (Esc closes game)");
Switch {
text: @tr("Fullscreen");
checked: SettingsData.fullscreen;
toggled => {
SettingsData.fullscreen = self.checked;
@@ -60,8 +62,8 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Widescreen (strech)");
Switch {
text: @tr("Widescreen (stretch)");
checked: SettingsData.widescreen;
toggled => {
SettingsData.widescreen = self.checked;
@@ -71,7 +73,18 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("VSync");
checked: SettingsData.vsync;
toggled => {
SettingsData.vsync = self.checked;
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Apply CRT shader");
checked: SettingsData.apply_crt_shader;
toggled => {
@@ -86,7 +99,7 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("Overclock N64 CPU (may cause bugs)");
checked: SettingsData.overclock_n64_cpu;
toggled => {
@@ -97,7 +110,7 @@ export component Settings inherits Page {
HorizontalBox {
alignment: start;
CheckBox {
Switch {
text: @tr("Disable Expansion Pak");
checked: SettingsData.disable_expansion_pak;
toggled => {
@@ -105,5 +118,16 @@ export component Settings inherits Page {
}
}
}
HorizontalBox {
alignment: start;
Switch {
text: @tr("Emulate UNFLoader");
checked: SettingsData.emulate_usb;
toggled => {
SettingsData.emulate_usb = self.checked;
}
}
}
}
}
+114 -41
View File
@@ -28,6 +28,8 @@ const Y_AXIS_SHIFT: usize = 24;
const MAX_AXIS_VALUE: f64 = 85.0;
pub const DEADZONE_DEFAULT: i32 = 5;
pub struct Controllers {
pub rumble: bool,
pub game_controller: *mut sdl3_sys::gamepad::SDL_Gamepad,
@@ -40,7 +42,7 @@ pub struct InputData {
}
fn bound_axis(x: &mut f64, y: &mut f64) {
let radius = 95.0; // this is roughly the maxium diagonal distance of the controller
let radius = f64::sqrt(70.0 * 70.0 + 70.0 * 70.0); // this is roughly the maximum diagonal distance of the controller
// Calculate the distance from the origin (0, 0)
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
@@ -53,6 +55,28 @@ fn bound_axis(x: &mut f64, y: &mut f64) {
}
}
fn apply_deadzone(x: &mut f64, y: &mut f64, deadzone: i32) {
let axis_deadzone = MAX_AXIS_VALUE * (deadzone as f64 / 100.0);
// Calculate the distance from the origin (0, 0)
let distance = f64::sqrt((*x) * (*x) + (*y) * (*y));
if distance <= axis_deadzone {
*x = 0.0;
*y = 0.0;
return;
}
let new_distance =
(distance - axis_deadzone) * MAX_AXIS_VALUE / (MAX_AXIS_VALUE - axis_deadzone);
*x = *x / distance * new_distance;
*y = *y / distance * new_distance;
}
fn normalize_axis_position(axis_position: i16) -> f64 {
axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64
}
fn set_axis_from_joystick(
profile: &ui::config::InputProfile,
joystick: *mut sdl3_sys::joystick::SDL_Joystick,
@@ -64,7 +88,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_LEFT].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_LEFT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.joystick_axis[AXIS_RIGHT].enabled {
@@ -72,7 +96,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_RIGHT].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_RIGHT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.joystick_axis[AXIS_DOWN].enabled {
@@ -80,7 +104,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_DOWN].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_DOWN].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
if profile.joystick_axis[AXIS_UP].enabled {
@@ -88,7 +112,7 @@ fn set_axis_from_joystick(
sdl3_sys::joystick::SDL_GetJoystickAxis(joystick, profile.joystick_axis[AXIS_UP].id)
};
if axis_position as isize * profile.joystick_axis[AXIS_UP].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
(x, y)
@@ -123,6 +147,17 @@ fn get_button_from_i32(button: i32) -> sdl3_sys::gamepad::SDL_GamepadButton {
12 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_DOWN,
13 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_LEFT,
14 => sdl3_sys::gamepad::SDL_GamepadButton::DPAD_RIGHT,
15 => sdl3_sys::gamepad::SDL_GamepadButton::MISC1,
16 => sdl3_sys::gamepad::SDL_GamepadButton::RIGHT_PADDLE1,
17 => sdl3_sys::gamepad::SDL_GamepadButton::LEFT_PADDLE1,
18 => sdl3_sys::gamepad::SDL_GamepadButton::RIGHT_PADDLE2,
19 => sdl3_sys::gamepad::SDL_GamepadButton::LEFT_PADDLE2,
20 => sdl3_sys::gamepad::SDL_GamepadButton::TOUCHPAD,
21 => sdl3_sys::gamepad::SDL_GamepadButton::MISC2,
22 => sdl3_sys::gamepad::SDL_GamepadButton::MISC3,
23 => sdl3_sys::gamepad::SDL_GamepadButton::MISC4,
24 => sdl3_sys::gamepad::SDL_GamepadButton::MISC5,
25 => sdl3_sys::gamepad::SDL_GamepadButton::MISC6,
_ => panic!("Invalid button"),
}
}
@@ -141,7 +176,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_LEFT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.controller_axis[AXIS_RIGHT].enabled {
@@ -152,7 +187,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_RIGHT].axis as isize > 0 {
x = axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64;
x = normalize_axis_position(axis_position);
}
}
if profile.controller_axis[AXIS_DOWN].enabled {
@@ -163,7 +198,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_DOWN].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
if profile.controller_axis[AXIS_UP].enabled {
@@ -174,7 +209,7 @@ fn set_axis_from_controller(
)
};
if axis_position as isize * profile.controller_axis[AXIS_UP].axis as isize > 0 {
y = (axis_position as f64 * MAX_AXIS_VALUE / i16::MAX as f64).neg();
y = normalize_axis_position(axis_position).neg();
}
}
(x, y)
@@ -225,8 +260,9 @@ fn set_buttons_from_joystick(
let profile_joystick_hat = profile.joystick_hat[i];
if profile_joystick_hat.enabled
&& unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, profile_joystick_hat.id) }
== profile_joystick_hat.direction
&& (unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, profile_joystick_hat.id) }
& profile_joystick_hat.direction)
!= 0
{
*keys |= 1 << i;
}
@@ -288,7 +324,7 @@ pub fn set_rumble(ui: &ui::Ui, channel: usize, rumble: u8) {
controller,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u32 * 60000,
(rumble & 1) as u32 * sdl3_sys::haptic::SDL_HAPTIC_INFINITY,
)
};
} else if !joystick.is_null() {
@@ -297,7 +333,7 @@ pub fn set_rumble(ui: &ui::Ui, channel: usize, rumble: u8) {
joystick,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u16 * u16::MAX,
(rumble & 1) as u32 * 60000,
(rumble & 1) as u32 * sdl3_sys::haptic::SDL_HAPTIC_INFINITY,
)
};
}
@@ -326,8 +362,9 @@ fn change_paks(
pressed =
unsafe { sdl3_sys::joystick::SDL_GetJoystickButton(joystick, joystick_button.id) };
} else if joystick_hat.enabled && !joystick.is_null() {
pressed = unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, joystick_hat.id) }
== joystick_hat.direction;
pressed = (unsafe { sdl3_sys::joystick::SDL_GetJoystickHat(joystick, joystick_hat.id) }
& joystick_hat.direction)
!= 0;
} else if key.enabled {
pressed = unsafe { *keyboard_state.offset(key.id as isize) };
}
@@ -347,6 +384,7 @@ pub fn get_controller_names(game_ui: &ui::Ui) -> Vec<String> {
controllers
}
#[cfg(feature = "gui")]
pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
let mut controller_paths: Vec<Option<String>> = vec![];
@@ -363,8 +401,6 @@ pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
}
pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
unsafe { sdl3_sys::events::SDL_PumpEvents() };
let profile_name = ui.config.input.input_profile_binding[channel].clone();
let profile = ui.config.input.input_profiles.get(&profile_name).unwrap();
let mut keys = 0;
@@ -407,9 +443,11 @@ pub fn get(ui: &ui::Ui, channel: usize) -> InputData {
}
if !controller.is_null() {
(x, y) = set_axis_from_controller(profile, controller)
(x, y) = set_axis_from_controller(profile, controller);
apply_deadzone(&mut x, &mut y, profile.deadzone);
} else if !joystick.is_null() {
(x, y) = set_axis_from_joystick(profile, joystick)
(x, y) = set_axis_from_joystick(profile, joystick);
apply_deadzone(&mut x, &mut y, profile.deadzone);
}
bound_axis(&mut x, &mut y);
@@ -452,9 +490,13 @@ pub fn clear_bindings(ui: &mut ui::Ui) {
}
}
fn close_controllers(
fn close_input_profile_window(
open_joysticks: Vec<*mut sdl3_sys::joystick::SDL_Joystick>,
open_controllers: Vec<*mut sdl3_sys::gamepad::SDL_Gamepad>,
renderer: *mut sdl3_sys::render::SDL_Renderer,
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
window: *mut sdl3_sys::video::SDL_Window,
) {
for joystick in open_joysticks {
unsafe { sdl3_sys::joystick::SDL_CloseJoystick(joystick) }
@@ -462,10 +504,18 @@ fn close_controllers(
for controller in open_controllers {
unsafe { sdl3_sys::gamepad::SDL_CloseGamepad(controller) }
}
unsafe {
sdl3_ttf_sys::ttf::TTF_CloseFont(font);
sdl3_ttf_sys::ttf::TTF_DestroyRendererTextEngine(text_engine);
sdl3_sys::render::SDL_DestroyRenderer(renderer);
sdl3_sys::video::SDL_DestroyWindow(window);
}
}
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool, deadzone: i32) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init();
if profile == "default" {
println!("Profile name cannot be default");
@@ -498,9 +548,9 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
if !unsafe {
sdl3_sys::render::SDL_CreateWindowAndRenderer(
title.as_ptr(),
640,
852,
480,
0,
sdl3_sys::video::SDL_WindowFlags(0),
&mut window,
&mut renderer,
)
@@ -567,26 +617,43 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
axis: 0,
};
let font =
ab_glyph::FontRef::try_from_slice(include_bytes!("../../data/Roboto-Regular.ttf")).unwrap();
let text_engine = unsafe { sdl3_ttf_sys::ttf::TTF_CreateRendererTextEngine(renderer) };
let font = unsafe {
let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf");
sdl3_ttf_sys::ttf::TTF_OpenFontIO(
sdl3_sys::everything::SDL_IOFromConstMem(
font_bytes.as_ptr() as *const std::ffi::c_void,
font_bytes.len(),
),
true,
35.0,
)
};
for (key, value) in key_labels.iter() {
let mut event: sdl3_sys::events::SDL_Event = Default::default();
while unsafe { sdl3_sys::events::SDL_PollEvent(&mut event) } {} // clear events
ui::video::draw_text(
format!("Select binding for: {key}").as_str(),
renderer,
&font,
);
let mut key_set = false;
while !key_set {
ui::video::draw_text(
format!("Select binding for: {key}").as_str(),
renderer,
text_engine,
font,
);
std::thread::sleep(std::time::Duration::from_millis(100));
while unsafe { sdl3_sys::events::SDL_PollEvent(&mut event) } {
let event_type = unsafe { event.r#type };
if event_type == u32::from(sdl3_sys::events::SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
close_controllers(open_joysticks, open_controllers);
close_input_profile_window(
open_joysticks,
open_controllers,
renderer,
text_engine,
font,
window,
);
return;
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_KEY_DOWN) {
if unsafe {
@@ -666,14 +733,18 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
}
}
}
ui::video::draw_text("Binding set, please wait...", renderer, text_engine, font);
std::thread::sleep(std::time::Duration::from_millis(500));
}
close_controllers(open_joysticks, open_controllers);
unsafe {
sdl3_sys::render::SDL_DestroyRenderer(renderer);
sdl3_sys::video::SDL_DestroyWindow(window);
}
close_input_profile_window(
open_joysticks,
open_controllers,
renderer,
text_engine,
font,
window,
);
let new_profile = ui::config::InputProfile {
keys: new_keys,
@@ -683,6 +754,7 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
joystick_hat: new_joystick_hat,
joystick_axis: new_joystick_axis,
dinput,
deadzone,
};
ui.config.input.input_profiles.insert(profile, new_profile);
}
@@ -883,6 +955,7 @@ pub fn get_default_profile() -> ui::config::InputProfile {
axis: 0,
}; PROFILE_SIZE],
dinput: false,
deadzone: DEADZONE_DEFAULT,
}
}
@@ -896,7 +969,7 @@ pub fn init(ui: &mut ui::Ui) {
for i in 0..4 {
let controller_assignment = &ui.config.input.controller_assignment[i];
if controller_assignment.is_some() && ui.config.input.controller_enabled[i] {
let mut joystick_id = 0;
let mut joystick_id = sdl3_sys::everything::SDL_JoystickID(0);
let assigned_path = controller_assignment.as_ref().unwrap();
for joystick in ui.input.joysticks.iter() {
@@ -923,7 +996,7 @@ pub fn init(ui: &mut ui::Ui) {
if !profile.dinput {
let gamepad = unsafe { sdl3_sys::gamepad::SDL_OpenGamepad(joystick_id) };
if gamepad.is_null() {
println!("could not connect gamepad: {joystick_id}")
println!("could not connect gamepad: {}", u32::from(joystick_id))
} else {
ui.input.controllers[i].game_controller = gamepad;
let properties =
@@ -942,7 +1015,7 @@ pub fn init(ui: &mut ui::Ui) {
} else {
let joystick = unsafe { sdl3_sys::joystick::SDL_OpenJoystick(joystick_id) };
if joystick.is_null() {
println!("could not connect joystick: {joystick_id}")
println!("could not connect joystick: {}", u32::from(joystick_id))
} else {
ui.input.controllers[i].joystick = joystick;
let properties =
+406 -299
View File
File diff suppressed because it is too large Load Diff
+120
View File
@@ -0,0 +1,120 @@
use crate::retroachievements;
use crate::ui;
use slint::ComponentHandle;
pub fn ra_window(app: &ui::gui::AppWindow) {
let mut token = String::new();
if let Ok(ra_config) = std::fs::read(ui::get_dirs().config_dir.join("retroachievements.json"))
&& let Ok(result) =
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
{
app.set_ra_username(result.username.into());
app.set_ra_enabled(result.enabled);
app.set_ra_hardcore(result.hardcore);
app.set_ra_challenge(result.challenge);
app.set_ra_leaderboard(result.leaderboard);
token = result.token;
} else {
app.set_ra_hardcore(true);
}
if !cfg!(ra_hardcore_enabled) {
app.set_ra_softcore_only(true);
}
if app.get_ra_enabled() && !app.get_ra_username().is_empty() {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
retroachievements::login_token_user(app.get_ra_username().to_string(), token, tx);
set_current_user_message(app, rx);
} else {
app.set_ra_current_user_message("Not currently logged in".into());
}
let weak_app2 = app.as_weak();
app.on_ra_button_clicked(move |password| {
weak_app2
.upgrade_in_event_loop(move |handle| {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
retroachievements::login_user(
handle.get_ra_username().to_string(),
password.to_string(),
tx,
);
set_current_user_message(&handle, rx);
})
.unwrap();
});
app.on_ra_toggled(move |enabled, hardcore, challenge, leaderboard| {
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
&& let Ok(result) =
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
{
if !enabled {
retroachievements::logout_user();
retroachievements::RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
} else {
retroachievements::RAConfig {
username: result.username,
token: result.token,
enabled,
hardcore,
challenge,
leaderboard,
}
}
} else {
retroachievements::RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
});
app.on_ra_games_clicked(move || {
open::that_detached("https://retroachievements.org/system/2-nintendo-64/games").unwrap();
});
app.on_ra_show_profile_clicked(move || {
open::that_detached(format!(
"https://retroachievements.org/user/{}",
retroachievements::get_username().unwrap_or_default()
))
.unwrap();
});
}
fn set_current_user_message(app: &ui::gui::AppWindow, rx: tokio::sync::oneshot::Receiver<bool>) {
app.set_ra_current_user_message("Logging in...".into());
let weak_app = app.as_weak();
tokio::spawn(async move {
rx.await.unwrap();
weak_app
.upgrade_in_event_loop(move |handle| {
if let Some(username) = retroachievements::get_username() {
handle.set_ra_current_user_message(format!("Logged in as {}", username).into());
handle.set_ra_show_profile(true);
} else {
handle.set_ra_current_user_message("Login failed".into());
handle.set_ra_show_profile(false);
}
handle.set_ra_logging_in(false);
})
.unwrap();
});
}
+82 -47
View File
@@ -28,13 +28,13 @@ pub struct Paths {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Save {
pub data: Vec<u8>,
pub written: bool,
pub write_pending: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct RomSave {
pub data: std::collections::HashMap<u32, u8>,
pub written: bool,
pub write_pending: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -135,13 +135,13 @@ pub fn get_game_crc(rom: &[u8]) -> String {
}
pub fn get_game_name(rom: &[u8]) -> String {
let mut game_name = "".to_owned();
let header_value = std::str::from_utf8(&rom[0x20..0x34]);
if let Ok(header_value) = header_value {
let re = regex::Regex::new(r"[^a-zA-Z0-9_ -]").unwrap();
game_name = re.replace_all(header_value, "").trim().replace('\0', "");
re.replace_all(header_value, "").trim().replace('\0', "")
} else {
String::new()
}
game_name
}
pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
@@ -163,46 +163,46 @@ pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
ui.storage
.paths
.eep_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".eep");
.push(format!("{}-{}.eep", prefix, ui.game_hash));
ui.storage.paths.sra_file_path.clone_from(&saves_path);
ui.storage
.paths
.sra_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".sra");
.push(format!("{}-{}.sra", prefix, ui.game_hash));
ui.storage.paths.fla_file_path.clone_from(&saves_path);
ui.storage
.paths
.fla_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".fla");
.push(format!("{}-{}.fla", prefix, ui.game_hash));
ui.storage.paths.pak_file_path.clone_from(&saves_path);
ui.storage
.paths
.pak_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".mpk");
.push(format!("{}-{}.mpk", prefix, ui.game_hash));
ui.storage.paths.sdcard_file_path.clone_from(&saves_path);
ui.storage
.paths
.sdcard_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".img");
.push(format!("{}-{}.img", prefix, ui.game_hash));
ui.storage.paths.romsave_file_path.clone_from(&saves_path);
ui.storage
.paths
.romsave_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".romsave");
.push(format!("{}-{}.romsave", prefix, ui.game_hash));
ui.storage
.paths
.savestate_file_path
.clone_from(&states_path);
ui.storage
.paths
.savestate_file_path
.push(prefix.to_owned() + "-" + &ui.game_hash + ".state");
ui.storage.paths.savestate_file_path.push(format!(
"{}-{}.state{}",
prefix, ui.game_hash, ui.storage.save_state_slot
));
}
pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
@@ -351,15 +351,10 @@ pub fn compress_file(data: &[(&[u8], &str)]) -> Vec<u8> {
compressed_file
}
fn write_rom_save(ui: &ui::Ui) {
let data = postcard::to_stdvec(&ui.storage.saves.romsave.data).unwrap();
std::fs::write(ui.storage.paths.romsave_file_path.clone(), data).unwrap();
}
fn writeback_sdcard(device: &mut device::Device) {
let length;
let save_data: &[u8];
if device.ui.storage.saves.eeprom.written {
if device.ui.storage.saves.eeprom.write_pending {
if device
.ui
.storage
@@ -371,12 +366,15 @@ fn writeback_sdcard(device: &mut device::Device) {
length = 4;
}
save_data = device.ui.storage.saves.eeprom.data.as_ref();
} else if device.ui.storage.saves.sram.written {
device.ui.storage.saves.eeprom.write_pending = false;
} else if device.ui.storage.saves.sram.write_pending {
length = device.ui.storage.saves.sram.data.len() / 512;
save_data = device.ui.storage.saves.sram.data.as_ref();
} else if device.ui.storage.saves.flash.written {
device.ui.storage.saves.sram.write_pending = false;
} else if device.ui.storage.saves.flash.write_pending {
length = device.ui.storage.saves.flash.data.len() / 512;
save_data = device.ui.storage.saves.flash.data.as_ref();
device.ui.storage.saves.flash.write_pending = false;
} else {
return;
}
@@ -386,71 +384,108 @@ fn writeback_sdcard(device: &mut device::Device) {
device.ui.storage.saves.sdcard.data[offset..offset + 512]
.copy_from_slice(&save_data[i * 512..(i + 1) * 512]);
}
device.ui.storage.saves.sdcard.written = true;
device.ui.storage.saves.sdcard.write_pending = true;
}
pub fn write_saves(device: &mut device::Device) {
if device.netplay.is_none() || device.netplay.as_ref().unwrap().player_number == 0 {
if device.ui.storage.saves.write_to_disk {
if device.ui.storage.saves.eeprom.written {
write_save(&device.ui, SaveTypes::Eeprom16k)
if device.ui.storage.saves.eeprom.write_pending {
write_save(&mut device.ui, SaveTypes::Eeprom16k)
}
if device.ui.storage.saves.sram.written {
write_save(&device.ui, SaveTypes::Sram)
if device.ui.storage.saves.sram.write_pending {
write_save(&mut device.ui, SaveTypes::Sram)
}
if device.ui.storage.saves.flash.written {
write_save(&device.ui, SaveTypes::Flash)
if device.ui.storage.saves.flash.write_pending {
write_save(&mut device.ui, SaveTypes::Flash)
}
if device.ui.storage.saves.romsave.written {
write_save(&device.ui, SaveTypes::Romsave)
if device.ui.storage.saves.romsave.write_pending {
write_save(&mut device.ui, SaveTypes::Romsave)
}
} else {
writeback_sdcard(device)
}
if device.ui.storage.saves.mempak.written {
write_save(&device.ui, SaveTypes::Mempak)
if device.ui.storage.saves.mempak.write_pending {
write_save(&mut device.ui, SaveTypes::Mempak)
}
if device.ui.storage.saves.sdcard.written {
write_save(&device.ui, SaveTypes::Sdcard)
if device.ui.storage.saves.sdcard.write_pending {
write_save(&mut device.ui, SaveTypes::Sdcard)
}
}
}
fn write_save(ui: &ui::Ui, save_type: SaveTypes) {
pub fn save_event(device: &mut device::Device) {
write_saves(device);
}
pub fn schedule_save(device: &mut device::Device, save_type: SaveTypes) {
match save_type {
SaveTypes::Eeprom4k | SaveTypes::Eeprom16k => {
device.ui.storage.saves.eeprom.write_pending = true;
}
SaveTypes::Sram => {
device.ui.storage.saves.sram.write_pending = true;
}
SaveTypes::Flash => {
device.ui.storage.saves.flash.write_pending = true;
}
SaveTypes::Mempak => {
device.ui.storage.saves.mempak.write_pending = true;
}
SaveTypes::Sdcard => {
device.ui.storage.saves.sdcard.write_pending = true;
}
SaveTypes::Romsave => {
device.ui.storage.saves.romsave.write_pending = true;
}
}
device::events::create_event(
device,
device::events::EVENT_TYPE_SAVE,
device.cpu.clock_rate, // 1 second
);
}
fn write_save(ui: &mut ui::Ui, save_type: SaveTypes) {
let path: &std::path::Path;
let data: &Vec<u8>;
let rom_save_data: Vec<u8>;
match save_type {
SaveTypes::Eeprom4k | SaveTypes::Eeprom16k => {
path = ui.storage.paths.eep_file_path.as_ref();
data = ui.storage.saves.eeprom.data.as_ref();
ui.storage.saves.eeprom.write_pending = false;
}
SaveTypes::Sram => {
path = ui.storage.paths.sra_file_path.as_ref();
data = ui.storage.saves.sram.data.as_ref();
ui.storage.saves.sram.write_pending = false;
}
SaveTypes::Flash => {
path = ui.storage.paths.fla_file_path.as_ref();
data = ui.storage.saves.flash.data.as_ref();
ui.storage.saves.flash.write_pending = false;
}
SaveTypes::Mempak => {
path = ui.storage.paths.pak_file_path.as_ref();
data = ui.storage.saves.mempak.data.as_ref();
ui.storage.saves.mempak.write_pending = false;
}
SaveTypes::Sdcard => {
path = ui.storage.paths.sdcard_file_path.as_ref();
data = ui.storage.saves.sdcard.data.as_ref();
ui.storage.saves.sdcard.write_pending = false;
}
SaveTypes::Romsave => {
write_rom_save(ui);
return;
path = ui.storage.paths.romsave_file_path.as_ref();
rom_save_data = postcard::to_stdvec(&ui.storage.saves.romsave.data).unwrap();
data = rom_save_data.as_ref();
ui.storage.saves.romsave.write_pending = false;
}
}
let result = std::fs::write(path, data);
if result.is_err() {
panic!(
"could not save {} {}",
path.display(),
result.err().unwrap()
)
}
let save_data = data.clone();
let save_path = path.to_path_buf();
tokio::spawn(async move {
tokio::fs::write(save_path, save_data).await.unwrap();
});
}
+178
View File
@@ -0,0 +1,178 @@
use crate::ui;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
const DATATYPE_TCPTEST: u32 = 0x07;
const DATATYPE_ROMUPLOAD: u32 = 0x08;
#[derive(Clone, Debug)]
pub struct UsbData {
pub data: Vec<u8>,
pub data_type: u32,
pub data_size: u32,
}
fn respond_to_handshake(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, data: Vec<u8>) {
if let Ok(data) = String::from_utf8(data)
&& data == "N64"
{
ui::usb::send_to_usb(
usb_tx,
ui::usb::UsbData {
data: [b'N', b'6', b'4'].to_vec(),
data_type: DATATYPE_TCPTEST,
data_size: 3,
},
);
}
}
async fn handle_connection(
conn: tokio::net::TcpStream,
mut shutdown_rx: tokio::sync::watch::Receiver<()>,
mut usb_rx: tokio::sync::broadcast::Receiver<UsbData>,
usb_tx: tokio::sync::broadcast::Sender<UsbData>,
cart_tx: tokio::sync::broadcast::Sender<UsbData>,
) {
let (mut incoming, mut outgoing) = conn.into_split();
let mut shutdown_rx_clone = shutdown_rx.clone();
tokio::spawn(async move {
loop {
tokio::select! {
data = usb_rx.recv() => {
match data {
Ok(data) => {
let mut output: Vec<u8> = vec![];
output.extend_from_slice(&data.data_type.to_be_bytes());
output.extend_from_slice(&data.data_size.to_be_bytes());
output.extend_from_slice(&data.data);
if outgoing.write_all(&output).await.is_err() {
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
panic!("usb_rx lagged");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
}
}
}
_ = shutdown_rx_clone.changed() => {
break;
}
}
}
});
let mut incoming_buffer = vec![0u8; 4096];
let mut data_type: Option<u32> = None;
let mut data_size: Option<u32> = None;
let mut usb_buffer: Vec<u8> = vec![];
loop {
tokio::select! {
result = incoming.read(&mut incoming_buffer) => {
match result {
Ok(0) => {
break;
}
Ok(n) => {
usb_buffer.extend_from_slice(&incoming_buffer[0..n]);
if data_type.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_type = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if data_type.is_some() && data_size.is_none() {
if usb_buffer.len() < 4 {
continue;
} else {
data_size = Some(u32::from_be_bytes(usb_buffer[0..4].try_into().unwrap()));
usb_buffer.drain(0..4);
}
}
if let Some(d_type) = data_type && let Some(d_size) = data_size {
let length = d_size as usize;
if usb_buffer.len() >= length {
let usb_data = UsbData {
data: usb_buffer[0..length].to_vec(),
data_type: d_type,
data_size: d_size,
};
usb_buffer.drain(0..length);
if usb_data.data_type == DATATYPE_TCPTEST {
respond_to_handshake(&usb_tx,usb_data.data);
} else if usb_data.data_type == DATATYPE_ROMUPLOAD {
panic!("ROM upload not supported");
} else {
cart_tx.send(usb_data).unwrap();
}
data_type = None;
data_size = None;
}
}
}
Err(_e) => {
break;
}
}
}
_ = shutdown_rx.changed() => {
break;
}
}
}
}
pub fn init() -> (Option<tokio::sync::watch::Sender<()>>, ui::Usb) {
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(());
let (usb_tx, usb_rx): (
tokio::sync::broadcast::Sender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>,
) = tokio::sync::broadcast::channel(1024);
let (cart_tx, cart_rx): (
tokio::sync::broadcast::Sender<UsbData>,
tokio::sync::broadcast::Receiver<UsbData>,
) = tokio::sync::broadcast::channel(1024);
let usb_tx_clone = usb_tx.clone();
let cart_rx_clone = cart_rx.resubscribe();
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind("localhost:64000")
.await
.unwrap();
loop {
tokio::select! {
res = listener.accept() => {
if let Ok((c,_)) = res {
handle_connection(c,shutdown_rx.clone(),usb_rx.resubscribe(),usb_tx.clone(),cart_tx.clone()).await;
} else {
break;
}
}
_ = shutdown_rx.changed() => {
break;
}
}
}
});
(
Some(shutdown_tx),
ui::Usb {
usb_tx: Some(usb_tx_clone),
cart_rx: Some(cart_rx_clone),
},
)
}
pub fn close(shutdown_tx: &tokio::sync::watch::Sender<()>) {
let _ = shutdown_tx.send(());
}
pub fn send_to_usb(usb_tx: &tokio::sync::broadcast::Sender<UsbData>, buffer: UsbData) {
usb_tx.send(buffer).unwrap();
}
+154 -99
View File
@@ -1,12 +1,42 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
use crate::{device, ui};
use ab_glyph::{Font, ScaleFont};
use crate::{device, retroachievements, ui};
const PAL_WIDESCREEN_WIDTH: i32 = 512;
const PAL_STANDARD_WIDTH: i32 = 384;
const PAL_HEIGHT: i32 = 288;
const NTSC_WIDESCREEN_WIDTH: i32 = 426;
const NTSC_STANDARD_WIDTH: i32 = 320;
const NTSC_HEIGHT: i32 = 240;
fn build_gfx_info(device: &mut device::Device) -> GFX_INFO {
GFX_INFO {
RDRAM: device.rdram.mem.as_mut_ptr(),
DMEM: device.rsp.mem.as_mut_ptr(),
RDRAM_SIZE: device.rdram.size,
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen,
vsync: if device.netplay.is_none() {
device.ui.config.video.vsync
} else {
false
},
integer_scaling: device.ui.config.video.integer_scaling,
upscale: device.ui.config.video.upscale,
crt: device.ui.config.video.crt,
}
}
pub fn init(device: &mut device::Device) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init();
let title = std::ffi::CString::new("gopher64").unwrap();
let window_title = std::ffi::CString::new("gopher64").unwrap();
let mut flags = sdl3_sys::video::SDL_WINDOW_VULKAN
| sdl3_sys::video::SDL_WINDOW_RESIZABLE
@@ -18,23 +48,28 @@ pub fn init(device: &mut device::Device) {
let window_width;
let window_height;
let scale = if device.ui.config.video.upscale > 1 {
device.ui.config.video.upscale as i32
} else {
2
};
if device.cart.pal {
window_width = if device.ui.config.video.widescreen {
1024
PAL_WIDESCREEN_WIDTH * scale
} else {
768
PAL_STANDARD_WIDTH * scale
};
window_height = 576;
window_height = PAL_HEIGHT * scale;
} else {
window_width = if device.ui.config.video.widescreen {
852
NTSC_WIDESCREEN_WIDTH * scale
} else {
640
NTSC_STANDARD_WIDTH * scale
};
window_height = 480;
window_height = NTSC_HEIGHT * scale;
}
device.ui.video.window = unsafe {
sdl3_sys::video::SDL_CreateWindow(title.as_ptr(), window_width, window_height, flags)
sdl3_sys::video::SDL_CreateWindow(window_title.as_ptr(), window_width, window_height, flags)
};
if device.ui.video.window.is_null() {
panic!("Could not create window: {}", unsafe {
@@ -52,29 +87,54 @@ pub fn init(device: &mut device::Device) {
}
unsafe {
sdl3_sys::everything::SDL_HideCursor();
let hint = std::ffi::CString::new("1").unwrap();
sdl3_sys::everything::SDL_SetHint(
sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
std::ffi::CString::new("1").unwrap().as_ptr(),
hint.as_ptr(),
);
}
let gfx_info = GFX_INFO {
RDRAM: device.rdram.mem.as_mut_ptr(),
DMEM: device.rsp.mem.as_mut_ptr(),
RDRAM_SIZE: device.rdram.size,
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen,
integer_scaling: device.ui.config.video.integer_scaling,
upscale: device.ui.config.video.upscale,
crt: device.ui.config.video.crt,
};
let gfx_info = build_gfx_info(device);
unsafe { rdp_init(device.ui.video.window as *mut std::ffi::c_void, gfx_info) }
unsafe {
let font_bytes = include_bytes!("../../data/RobotoMono-Regular.ttf");
rdp_init(
device.ui.video.window as *mut std::ffi::c_void,
gfx_info,
font_bytes.as_ptr() as *const std::ffi::c_void,
font_bytes.len(),
device.ui.storage.save_state_slot,
)
}
fps_counter(&mut device.ui);
}
fn fps_counter(ui: &mut ui::Ui) {
let mut fps_rx = ui.video.fps_rx.take().unwrap();
let mut vis_rx = ui.video.vis_rx.take().unwrap();
tokio::spawn(async move {
loop {
let mut fps: u32 = 0;
let mut vis: u32 = 0;
loop {
match fps_rx.try_recv() {
Ok(_fps_update) => fps += 1,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return,
}
}
loop {
match vis_rx.try_recv() {
Ok(_vis_update) => vis += 1,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => return,
}
}
unsafe { rdp_set_fps(fps, vis) };
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
}
pub fn close(ui: &ui::Ui) {
@@ -85,10 +145,13 @@ pub fn close(ui: &ui::Ui) {
}
pub fn update_screen() {
// when the window is closed, running is set to false
unsafe { rdp_update_screen() }
}
pub fn render_frame() {
unsafe { rdp_render_frame() }
}
pub fn state_size() -> usize {
unsafe { rdp_state_size() }
}
@@ -98,21 +161,7 @@ pub fn save_state(rdp_state: *mut u8) {
}
pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
let gfx_info = GFX_INFO {
RDRAM: device.rdram.mem.as_mut_ptr(),
DMEM: device.rsp.mem.as_mut_ptr(),
RDRAM_SIZE: device.rdram.size,
DPC_CURRENT_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_CURRENT_REG as usize],
DPC_START_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_START_REG as usize],
DPC_END_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_END_REG as usize],
DPC_STATUS_REG: &mut device.rdp.regs_dpc[device::rdp::DPC_STATUS_REG as usize],
PAL: device.cart.pal,
widescreen: device.ui.config.video.widescreen,
fullscreen: device.ui.video.fullscreen,
integer_scaling: device.ui.config.video.integer_scaling,
upscale: device.ui.config.video.upscale,
crt: device.ui.config.video.crt,
};
let gfx_info = build_gfx_info(device);
unsafe {
rdp_new_processor(gfx_info);
rdp_load_state(rdp_state);
@@ -122,9 +171,22 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
}
}
pub fn check_callback(device: &mut device::Device) -> bool {
pub fn pause_loop(frame_time: f64) {
let mut paused = true;
let mut frame_advance = false;
while paused && !frame_advance {
std::thread::sleep(std::time::Duration::from_secs_f64(frame_time));
unsafe { sdl3_sys::events::SDL_PumpEvents() };
retroachievements::do_idle();
let callback = unsafe { rdp_check_callback() };
paused = callback.paused;
frame_advance = callback.frame_advance;
}
}
pub fn check_callback(device: &mut device::Device) -> (bool, bool) {
let mut speed_limiter_toggled = false;
let mut callback = unsafe { rdp_check_callback() };
let callback = unsafe { rdp_check_callback() };
device.cpu.running = callback.emu_running;
if device.netplay.is_none() {
if callback.save_state {
@@ -132,23 +194,43 @@ pub fn check_callback(device: &mut device::Device) -> bool {
} else if callback.load_state {
device.load_state = true;
}
if callback.reset_game {
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 as usize] &=
!device::cop0::COP0_CAUSE_EXCCODE_MASK;
device::events::create_event(
device,
device::events::EVENT_TYPE_NMI,
device.cpu.clock_rate, // 1 second
);
}
if device.vi.enable_speed_limiter != callback.enable_speedlimiter {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
}
while callback.paused {
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
unsafe { sdl3_sys::events::SDL_PumpEvents() };
callback = unsafe { rdp_check_callback() };
}
}
if device.ui.storage.save_state_slot != callback.save_state_slot {
onscreen_message(
&format!("Switching savestate slot to {}", callback.save_state_slot),
false,
);
device.ui.storage.save_state_slot = callback.save_state_slot;
device
.ui
.storage
.paths
.savestate_file_path
.set_extension(format!("state{}", callback.save_state_slot));
}
if callback.lower_volume {
ui::audio::lower_audio_volume(&mut device.ui);
} else if callback.raise_volume {
ui::audio::raise_audio_volume(&mut device.ui);
}
speed_limiter_toggled
(speed_limiter_toggled, callback.paused)
}
pub fn set_register(reg: u32, value: u32) {
@@ -161,60 +243,33 @@ pub fn process_rdp_list() -> u64 {
unsafe { rdp_process_commands() }
}
pub fn check_framebuffers(address: u32, length: u32) {
unsafe { rdp_check_framebuffers(address, length) }
}
pub fn onscreen_message(message: &str, long_message: bool) {
unsafe {
let c_message = std::ffi::CString::new(message).unwrap();
rdp_onscreen_message(c_message.as_ptr(), long_message)
};
}
pub fn draw_text(
text: &str,
renderer: *mut sdl3_sys::render::SDL_Renderer,
font: &ab_glyph::FontRef,
text_engine: *mut sdl3_ttf_sys::ttf::TTF_TextEngine,
font: *mut sdl3_ttf_sys::ttf::TTF_Font,
) {
// Clear the canvas
unsafe {
sdl3_sys::render::SDL_SetRenderDrawColor(
renderer,
0,
0,
0,
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
);
sdl3_sys::render::SDL_RenderClear(renderer);
};
let (mut w, mut h) = (0, 0);
sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h);
let text_size = 40.0;
let (mut w, mut h) = (0, 0);
unsafe { sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h) };
let x_start = 20.0;
let y_start = (h / 2) as f32;
let c_text = std::ffi::CString::new(text).unwrap();
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(text_engine, font, c_text.as_ptr(), 0);
let mut x_offset = 0.0;
for c in text.chars() {
let q_glyph_id = font.glyph_id(c);
let q_glyph = q_glyph_id.with_scale(text_size);
if let Some(q) = font.outline_glyph(q_glyph) {
q.draw(|x, y, c| {
if c > 0.5 {
unsafe {
sdl3_sys::render::SDL_SetRenderDrawColor(
renderer,
255,
255,
255,
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
);
sdl3_sys::render::SDL_RenderPoint(
renderer,
x_start + x_offset + x as f32 - q.px_bounds().width()
+ q.px_bounds().max.x,
y_start + y as f32 - q.px_bounds().height() + q.px_bounds().max.y,
);
};
}
});
}
x_offset += font.as_scaled(text_size).h_advance(q_glyph_id);
}
// Present the canvas
if !unsafe { sdl3_sys::render::SDL_RenderPresent(renderer) } {
panic!("Could not present renderer");
sdl3_sys::everything::SDL_RenderClear(renderer);
sdl3_ttf_sys::ttf::TTF_DrawRendererText(ttf_text, 20.0, h as f32 / 2.0);
sdl3_sys::render::SDL_RenderPresent(renderer);
sdl3_ttf_sys::ttf::TTF_DestroyText(ttf_text);
}
}
+42 -15
View File
@@ -1,21 +1,48 @@
pub fn prompt_for_match(
words: &[String],
window_notifier: &tokio::sync::mpsc::Sender<Option<Vec<String>>>,
word_index_receiver: &mut tokio::sync::mpsc::Receiver<String>,
) -> u16 {
use crate::retroachievements;
use crate::ui::gui;
use slint::ComponentHandle;
pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 {
let mut dedup_words = words.to_owned();
dedup_words.sort();
dedup_words.dedup();
window_notifier.try_send(Some(dedup_words)).unwrap();
let mut result = word_index_receiver.try_recv();
while result.is_err() {
result = word_index_receiver.try_recv();
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the OS doesn't complain about the game window being frozen
}
for (i, v) in words.iter().enumerate() {
if *v == *result.as_ref().unwrap() {
return i as u16;
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let vru_dialog = gui::VruDialog::new().unwrap();
let vru_dialog_weak = vru_dialog.as_weak();
vru_dialog.on_vru_button_clicked(move |chosen_word| {
tx.try_send(chosen_word.to_string()).unwrap();
vru_dialog_weak.unwrap().window().hide().unwrap();
});
vru_dialog.set_words(slint::ModelRc::from(std::rc::Rc::new(
slint::VecModel::from(
dedup_words
.iter()
.map(|x| x.into())
.collect::<Vec<slint::SharedString>>(),
),
)));
let timer = slint::Timer::default();
timer.start(
slint::TimerMode::Repeated,
std::time::Duration::from_secs_f64(frame_time),
move || {
unsafe { sdl3_sys::events::SDL_PumpEvents() }; // so the OS doesn't complain about the game window being frozen
retroachievements::do_idle();
},
);
vru_dialog.run().unwrap();
if let Ok(result) = rx.try_recv() {
for (i, v) in words.iter().enumerate() {
if *v == result {
return i as u16;
}
}
}
0x7FFF