Compare commits

...

86 Commits

Author SHA1 Message Date
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
Logan McNaughton d7f656cd6a update cargo packages 2025-07-25 21:26:54 +02:00
Logan McNaughton 8558334391 Add code signing policy 2025-07-25 16:23:46 +02:00
Logan McNaughton b6de872d78 Sign code using SignPath (#525)
* Sign code using SignPath

* more

* more

* more

* more

* more

* use release-signing
2025-07-25 11:45:11 +02:00
Logan McNaughton 712dc10c65 cleanup netplay error checking 2025-07-25 07:20:50 +02:00
Logan McNaughton cd2b5b4e92 error checking for netplay ui channels (#527)
* error checking for netplay ui channels

* more
2025-07-25 07:00:45 +02:00
Logan McNaughton 98b5d6506e add spacing in settings 2025-07-20 23:12:01 +02:00
Logan McNaughton f2e8bc6356 data access fixes 2025-07-20 21:33:07 +02:00
Logan McNaughton 328e1730a0 Update parallel-rdp (#523) 2025-07-20 09:55:36 +02:00
Logan McNaughton 8010a6211c Use default AR on windows (#522)
* Use default AR on windows

* use llvm-ar

* more
2025-07-20 08:16:04 +02:00
Logan McNaughton 41b1fd05be build without clang-cl on Windows (#521) 2025-07-20 07:51:00 +02:00
Logan McNaughton c5da2a795f use specific llvm version (#520)
* use specific llvm version

* more

* more

* more

* more
2025-07-19 18:30:17 +02:00
Logan McNaughton 7361a9bd5c remove unneeded flag (#519) 2025-07-19 16:45:27 +02:00
Logan McNaughton d4e8933352 Use rust-lld on Windows (#518) 2025-07-19 16:31:58 +02:00
Logan McNaughton fff376db5e cargo update 2025-07-19 15:51:57 +02:00
Logan McNaughton d7f1c8415a disable cargo dependabot 2025-07-19 15:51:23 +02:00
Logan McNaughton ac09be4202 Increase axis movement requirement when configuring controller (#514) 2025-07-17 15:22:21 +02:00
Logan McNaughton 022190384a Add support for custom netplay server url (#511)
* Add support for custom netplay server url

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-07-16 15:42:54 +02:00
Logan McNaughton e4dd5c2dee Fix crash in wrestlemania (#510) 2025-07-16 12:13:12 +02:00
Logan McNaughton 3e95059ec7 add pause hotkey (#509) 2025-07-15 20:43:31 +02:00
Logan McNaughton abc39e74ef Remember ROM dir (#508)
* Remember ROM dir

* more

* more

* more

* more

* more
2025-07-15 20:05:46 +02:00
Logan McNaughton 7109d28223 update c standard (#507)
* update c standard

* more

* more
2025-07-15 15:28:34 +02:00
Logan McNaughton 1d1e3829e4 Update icon 2025-07-14 15:47:11 +02:00
Logan McNaughton a46e4f505b re-enable mac build 2025-07-13 17:51:37 +02:00
Logan McNaughton 03fbe658f2 Update netplay server url (#491) 2025-07-12 15:01:59 +02:00
Logan McNaughton b96c9e4cdc bump to 1.1.1 (#490) 2025-07-11 21:28:53 +02:00
Logan McNaughton cea405ea30 bump to 1.1.0 (#489) 2025-07-11 21:14:18 +02:00
Logan McNaughton c13d354201 savestate speed improvements (#486)
* savestate speed improvements

* more
2025-07-10 18:13:16 +02:00
Logan McNaughton ff43f59f74 fix compile warning (#485)
* fix compile warning

* more
2025-07-10 14:38:28 +02:00
dependabot[bot] eb97b96daf Bump zip from 4.2.0 to 4.3.0 (#483)
Bumps [zip](https://github.com/zip-rs/zip2) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: zip
  dependency-version: 4.3.0
  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-10 12:48:44 +02:00
Logan McNaughton 9bf43a16b4 Move SDL_PumpEvents (#482)
* Move SDL_PumpEvents

* more
2025-07-09 22:07:38 +02:00
Logan McNaughton b67247f935 Sync sdl window to prevent fullscreen issues (#481) 2025-07-09 19:55:24 +02:00
Logan McNaughton f10c591047 init rdp device 2025-07-09 10:45:12 +02:00
Logan McNaughton 82f1965f26 avoid syncing RDP too much (#479) 2025-07-09 10:31:20 +02:00
Logan McNaughton a5ddea353a print sdl errors (#478) 2025-07-09 10:19:44 +02:00
Logan McNaughton 6c977dbe5e Update README.md 2025-07-07 14:38:36 +02:00
Logan McNaughton 5dcfd72b18 bump to 1.0.21 (#477) 2025-07-06 20:07:31 +02:00
Logan McNaughton e578d6083d Don't use wgpu 2025-07-06 15:57:15 +02:00
Logan McNaughton e99a236070 use thin lto (#475) 2025-07-06 14:36:52 +02:00
Logan McNaughton e37633ecbd LTO2 (#474)
* LTO2

* more
2025-07-06 13:18:56 +02:00
Logan McNaughton d42e145b1f rename rdp build 2025-07-06 11:04:19 +02:00
Logan McNaughton 8ab31b0c28 Use Clang (#473) 2025-07-06 10:44:05 +02:00
Logan McNaughton 6d43191c56 cleanup release args 2025-07-06 07:28:34 +02:00
Logan McNaughton 75457c5f7a codegen-units = 16 2025-07-05 22:06:47 +02:00
Logan McNaughton 6d85358f2b re-order destruction of UI elements (#471)
* re-order destruction of UI elements

* more
2025-07-05 21:25:41 +02:00
Logan McNaughton ef4f93d6f0 bump to 1.0.20 (#470) 2025-07-05 18:14:18 +02:00
Logan McNaughton e4b7e291dd update socket2 (#469) 2025-07-05 17:52:29 +02:00
Logan McNaughton 16732d3d3c use is_multiple_of (#468) 2025-07-05 09:31:03 +02:00
Logan McNaughton 5857da0404 add cheats enabled var 2025-07-05 09:25:31 +02:00
Logan McNaughton 6c6d0f32a7 fix cheats 2025-07-04 22:33:25 +02:00
Logan McNaughton 7b400a0edc Update cheats (#467) 2025-07-04 22:13:52 +02:00
Logan McNaughton 8f6c6f1b94 Cheat support (#459)
* Cheat support

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more
2025-07-04 16:46:03 +02:00
Logan McNaughton 7fd43886de add a few vru phrases 2025-07-03 13:27:33 +02:00
Logan McNaughton 572807def0 add missing disable_expansion_pak in netplay features 2025-07-02 22:26:01 +02:00
Logan McNaughton 10a1e3a556 rename netplaydialog 2025-07-02 22:22:42 +02:00
Logan McNaughton c12dc460a7 update tokio 2025-07-02 22:17:34 +02:00
Logan McNaughton 72456b48e5 some code cleanup 2025-07-02 22:15:15 +02:00
Logan McNaughton 6ecb81fda1 move netplay_window to netplay.rs 2025-07-02 22:08:46 +02:00
Logan McNaughton 31b4e7944b non-empty default value for ram (#465) 2025-07-02 14:33:19 +02:00
Logan McNaughton dc1535ea04 better RAM access checks (#464)
* better RAM access checks

* more
2025-07-02 14:25:03 +02:00
Logan McNaughton 7a59714417 Make expansion pak optional (#463)
* Make expansion pak optional

* more
2025-07-02 09:59:01 +02:00
Logan McNaughton ead561933b Don't adjust variable limit after toggle (#458)
* Don't adjust variable limit after toggle

* more
2025-06-30 10:55:34 +02:00
Logan McNaughton 1d331e73fb cleanup aarch64 flags (#455) 2025-06-28 18:51:12 +02:00
Logan McNaughton 25aaa985f1 netplay feedback button (#454) 2025-06-28 16:50:59 +02:00
Logan McNaughton 11830b0222 add button for netplay discord (#452) 2025-06-28 14:19:50 +02:00
Logan McNaughton 3183a5eb0b add some more about buttons (#451) 2025-06-28 10:43:09 +02:00
Logan McNaughton 5d8ac37a0f Add new version button to main screen (#450) 2025-06-27 13:09:22 +02:00
Logan McNaughton baa6a6c635 Update download links 2025-06-27 10:15:25 +02:00
Logan McNaughton f389be0773 Fix netplay server url (#449) 2025-06-26 21:04:54 +02:00
Logan McNaughton 30eee885e4 Update to 1.0.19 (#448) 2025-06-26 20:58:41 +02:00
77 changed files with 2725 additions and 11085 deletions
+11
View File
@@ -1,5 +1,16 @@
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.'cfg(target_os = "windows")']
linker = "rust-lld"
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-C", "target-cpu=x86-64-v3"]
[target.'cfg(target_arch = "aarch64")']
rustflags = ["-C", "target-cpu=cortex-a76"]
[env]
CC = "clang"
CXX = "clang++"
AR = "llvm-ar"
+4 -4
View File
@@ -4,7 +4,7 @@ updates:
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
# - package-ecosystem: "cargo"
# directory: "/"
# schedule:
# interval: "daily"
+37 -1
View File
@@ -5,6 +5,8 @@ permissions:
on:
push:
branches: ["main"]
tags:
- "v*"
pull_request:
branches: ["main"]
@@ -21,9 +23,11 @@ jobs:
- os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
arch: linux-x86_64
llvm: X64
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
arch: linux-aarch64
llvm: ARM64
steps:
- uses: actions/checkout@v4
with:
@@ -32,6 +36,8 @@ jobs:
with:
target: ${{ matrix.target }}
rustflags: ""
- name: Set LLVM_VERSION
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
@@ -44,6 +50,11 @@ jobs:
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
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
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
@@ -64,9 +75,11 @@ jobs:
- os: windows-2025
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
steps:
- uses: actions/checkout@v4
with:
@@ -75,16 +88,37 @@ jobs:
with:
target: ${{ matrix.target }}
rustflags: ""
- name: Set LLVM_VERSION
run: |
$llvm_version = rustc --version --verbose | Select-String "LLVM version" | ForEach-Object { $_.Line.Split(":")[1].Trim() }
echo "LLVM_VERSION=$llvm_version" >> $env: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 }}-${{ matrix.llvm }}.exe
- name: Build
run: |
cargo build --release -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
- name: Upload file
- name: Upload unsigned artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
with:
name: gopher64-${{ matrix.arch }}
path: output/gopher64-${{ matrix.arch }}.exe
- name: SignPath
if: ${{ github.ref_type == 'tag' }}
uses: signpath/github-action-submit-signing-request@v1.3
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "${{ secrets.SIGNPATH_ORG_ID }}"
project-slug: "gopher64"
artifact-configuration-slug: "gopher64-${{ matrix.arch }}"
signing-policy-slug: "release-signing"
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
wait-for-completion: false
build-macos:
runs-on: macos-15
@@ -109,6 +143,8 @@ jobs:
cargo build --release -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
env:
AR: ar
- name: Upload file
uses: actions/upload-artifact@v4
with:
+7
View File
@@ -22,6 +22,8 @@ jobs:
- 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
- name: Install SDL Dependencies
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
uses: awalsh128/cache-apt-pkgs-action@v1
@@ -34,6 +36,11 @@ jobs:
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
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
- name: Rustfmt Check
uses: actions-rust-lang/rustfmt@v1
- name: Lint
+5 -1
View File
@@ -1,3 +1,7 @@
[submodule "parallel-rdp/parallel-rdp-standalone"]
path = parallel-rdp/parallel-rdp-standalone
url = https://github.com/Themaister/parallel-rdp-standalone.git
url = https://github.com/gopher64/parallel-rdp-standalone.git
branch = gopher64
[submodule "src/compat/sse2neon"]
path = src/compat/sse2neon
url = https://github.com/DLTcollab/sse2neon.git
Generated
+451 -723
View File
File diff suppressed because it is too large Load Diff
+9 -10
View File
@@ -1,34 +1,34 @@
[package]
name = "gopher64"
version = "1.0.18"
version = "1.1.3"
edition = "2024"
rust-version = "1.88"
rust-version = "1.89"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dirs = "6.0"
zip = "4.2"
zip = "4.3"
governor = "0.10"
sevenz-rust2 = { version = "0.15", default-features = false }
sevenz-rust2 = { version = "0.17", default-features = false }
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-wgpu"] }
slint = { version = "1.12", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-femtovg"] }
open = "5.3"
sha2 = "0.10"
ab_glyph = "0.2"
sdl3-sys = { version = "0.5", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.2", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"] }
tokio = {version = "1.45", features = ["rt-multi-thread", "macros"] }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros"] }
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
regex = "1.11"
tokio-tungstenite = "0.27"
futures = "0.3"
socket2 = "0.5"
socket2 = { version = "0.6", features = ["all"] }
rand_chacha = "0.9"
fatfs = "0.3"
semver = "1.0"
@@ -43,6 +43,5 @@ slint-build = "1.12"
cc = { version = "1.2", features = ["parallel"] }
[profile.release]
codegen-units = 1
strip = true
panic = "abort"
lto = "thin"
+10 -12
View File
@@ -1,7 +1,9 @@
# gopher64
## download
https://github.com/gopher64/gopher64/releases
Windows: https://github.com/gopher64/gopher64/releases/latest/download/gopher64-windows-x86_64.exe
Linux: https://flathub.org/apps/io.github.gopher64.gopher64
## wiki
@@ -15,13 +17,9 @@ https://discord.gg/9RGXq8W8JQ
Keys are mapped according to [these defaults](https://github.com/gopher64/gopher64/wiki/Default-Keyboard-Setup). Xbox-style controllers also have a [default mapping applied](https://github.com/gopher64/gopher64/wiki/Default-Gamepad-Setup).
You can create your mappings by running `./gopher64 --configure-input-profile my_profile`. You then bind that profile to a port: `./gopher64 --bind-input-profile my_profile --port 1`
To use a controller (for example, an Xbox controller), run `./gopher64 --list-controllers` to get a list of attached controllers, and then assign it by doing `./gopher64 --assign-controller <controller_number> --port 1`
## 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,12 +33,6 @@ If you want to run the flatpak from the command line, you need to add the `--fil
flatpak run --filesystem=host:ro io.github.gopher64.gopher64 /path/to/rom.z64
```
## goals
1. Performance. I want to be able to use this emulator on my laptop.
2. Easy to use.
3. Easy to work on. Dynamic recompilers perform well, but they are very hard to read and understand. This emulator will only have interpreters for the CPU and RSP. Additionally, it is completely written in Rust (besides Parallel-RDP), a modern programming language with a growing user base. I've tried to avoid the use of macros, which can reduce some repetitiveness in the code but also reduce readability.
## building and usage
1. Linux only: install the SDL3 dependencies: https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
@@ -57,3 +49,9 @@ 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.
## 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.
+48 -39
View File
@@ -6,11 +6,17 @@ fn main() {
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
let mut simd_build = cc::Build::new();
let mut build = cc::Build::new();
build
let mut volk_build = cc::Build::new();
volk_build
.std("c17")
.include("parallel-rdp/parallel-rdp-standalone/vulkan-headers/include")
.file("parallel-rdp/parallel-rdp-standalone/volk/volk.c");
let mut rdp_build = cc::Build::new();
rdp_build
.cpp(true)
.warnings(false)
.std("c++17")
.std("c++23")
.flag("-Wno-unused-parameter")
.flag("-Wno-missing-field-initializers")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/command_ring.cpp")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/rdp_device.cpp")
.file("parallel-rdp/parallel-rdp-standalone/parallel-rdp/rdp_dump_write.cpp")
@@ -30,6 +36,7 @@ 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")
@@ -47,7 +54,6 @@ fn main() {
.file("parallel-rdp/parallel-rdp-standalone/util/timeline_trace_file.cpp")
.file("parallel-rdp/parallel-rdp-standalone/util/environment.cpp")
.file("parallel-rdp/parallel-rdp-standalone/util/thread_name.cpp")
.file("parallel-rdp/parallel-rdp-standalone/volk/volk.c")
.file("parallel-rdp/interface.cpp")
.file("parallel-rdp/wsi_platform.cpp")
.include("parallel-rdp/parallel-rdp-standalone/parallel-rdp")
@@ -58,47 +64,44 @@ fn main() {
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_OUT_DIR").to_owned().unwrap())
.join("include"),
)
.include(
std::path::PathBuf::from(std::env::var("DEP_SDL3_TTF_OUT_DIR").to_owned().unwrap())
.join("include"),
);
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap();
let profile = std::env::var("PROFILE").unwrap();
let opt_flag = if arch == "x86_64" {
"-march=x86-64-v3"
} else if arch == "aarch64" {
"-march=armv8.2-a"
} else {
panic!("unknown arch")
};
volk_build.flag(opt_flag);
rdp_build.flag(opt_flag);
simd_build.flag(opt_flag);
if os == "windows" {
if arch == "x86_64" {
if env == "msvc" {
build.flag("/arch:AVX2");
} else if env == "gnu" {
build.flag("-march=x86-64-v3");
} else {
panic!("unknown env")
}
} else if arch == "aarch64" {
panic!("unsupported platform")
} else {
panic!("unknown arch")
}
build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
volk_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
rdp_build.flag("-DVK_USE_PLATFORM_WIN32_KHR");
winresource::WindowsResource::new()
.set_icon("data/icon.ico")
.set_icon("data/icon/icon.ico")
.compile()
.unwrap();
} else if os == "linux" || os == "macos" {
if arch == "x86_64" {
build.flag("-march=x86-64-v3");
} else if arch == "aarch64" {
build.flag("-march=armv8.2-a");
simd_build.flag("-march=armv8.2-a");
simd_build.flag("-DSSE2NEON_SUPPRESS_WARNINGS");
simd_build.file("src/compat/aarch64.c");
} else {
panic!("unknown arch")
}
} else {
panic!("unknown OS")
}
build.compile("parallel-rdp");
if profile == "release" {
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
simd_build.flag("-flto=thin");
}
volk_build.compile("volk");
rdp_build.compile("parallel-rdp");
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
@@ -109,6 +112,7 @@ fn main() {
.allowlist_function("rdp_set_vi_register")
.allowlist_function("rdp_update_screen")
.allowlist_function("rdp_process_commands")
.allowlist_function("rdp_onscreen_message")
.allowlist_function("rdp_check_callback")
.allowlist_function("rdp_new_processor")
.allowlist_function("rdp_state_size")
@@ -124,7 +128,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")
@@ -169,9 +173,14 @@ fn main() {
.write_to_file(out_path.join("simd_bindings.rs"))
.expect("Couldn't write bindings!");
simd_build.file(std::env::temp_dir().join("bindgen").join("extern.c"));
simd_build.include(".");
simd_build.compile("simd");
simd_build
.std("c17")
.flag("-D_POSIX_C_SOURCE=200112L")
.flag("-DSSE2NEON_SUPPRESS_WARNINGS")
.file("src/compat/aarch64.c")
.file(std::env::temp_dir().join("bindgen").join("extern.c"))
.include(".")
.compile("simd");
}
let git_output = std::process::Command::new("git")
+1
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 79 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
magick gopher64.png -resize 512x512 gopher64_512x512.png
magick gopher64.png -resize 256x256 gopher64_256x256.png
magick gopher64.png -resize 128x128 gopher64_128x128.png
magick gopher64.png -resize 256x256 icon.ico
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

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.
+110 -7
View File
@@ -5,6 +5,7 @@
#include "spirv.hpp"
#include "spirv_crt.hpp"
#include <SDL3/SDL_vulkan.h>
#include <SDL3_ttf/SDL_ttf.h>
using namespace Vulkan;
@@ -58,6 +59,8 @@ typedef struct
int cmd_cur;
int cmd_ptr;
uint32_t region;
uint64_t vi_counter;
uint64_t last_sync;
} RDP_DEVICE;
static SDL_Window *window;
@@ -72,6 +75,12 @@ static GFX_INFO gfx_info;
static const uint32_t *fragment_spirv;
static size_t fragment_size;
static TTF_Font *message_font;
static std::queue<std::string> messages;
static uint64_t message_timer;
#define MESSAGE_TIME 3000 // 3 seconds
typedef struct
{
float SourceSize[4];
@@ -149,6 +158,7 @@ bool sdl_event_filter(void *userdata, SDL_Event *event)
{
if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)
{
callback.paused = false;
callback.emu_running = false;
}
else if (event->type == SDL_EVENT_WINDOW_RESIZED && callback.emu_running)
@@ -172,6 +182,12 @@ bool sdl_event_filter(void *userdata, SDL_Event *event)
callback.enable_speedlimiter = !callback.enable_speedlimiter;
}
break;
case SDL_SCANCODE_P:
if (event->key.mod & SDL_KMOD_ALT)
{
callback.paused = !callback.paused;
}
break;
case SDL_SCANCODE_ESCAPE:
if (gfx_info.fullscreen)
callback.emu_running = false;
@@ -191,6 +207,24 @@ bool sdl_event_filter(void *userdata, SDL_Event *event)
case SDL_SCANCODE_RIGHTBRACKET:
callback.raise_volume = true;
break;
case SDL_SCANCODE_0:
case SDL_SCANCODE_1:
case SDL_SCANCODE_2:
case SDL_SCANCODE_3:
case SDL_SCANCODE_4:
case SDL_SCANCODE_5:
case SDL_SCANCODE_6:
case SDL_SCANCODE_7:
case SDL_SCANCODE_8:
case SDL_SCANCODE_9:
if (event->key.mod & SDL_KMOD_ALT)
{
if (event->key.scancode == SDL_SCANCODE_0)
callback.save_state_slot = 0;
else
callback.save_state_slot = event->key.scancode - SDL_SCANCODE_1 + 1;
}
break;
default:
break;
}
@@ -223,9 +257,28 @@ void rdp_new_processor(GFX_INFO _gfx_info)
processor = new RDP::CommandProcessor(wsi->get_device(), gfx_info.RDRAM, 0, gfx_info.RDRAM_SIZE, gfx_info.RDRAM_SIZE / 2, flags);
}
void rdp_init(void *_window, GFX_INFO _gfx_info)
static ImageHandle create_message_image(Vulkan::Device &device, int width, const char *message)
{
SDL_Color fg = {255, 255, 255, 255};
SDL_Color bg = {0, 0, 0, 0};
SDL_Surface *surface = TTF_RenderText_LCD_Wrapped(message_font, message, 0, fg, bg, width);
ImageCreateInfo info = ImageCreateInfo::immutable_2d_image(surface->w, surface->h, VK_FORMAT_B8G8R8A8_UNORM, false);
ImageInitialData initial_data = {};
initial_data.data = surface->pixels;
initial_data.row_length = surface->pitch / 4;
initial_data.image_height = surface->h;
ImageHandle handle = device.create_image(info, &initial_data);
SDL_DestroySurface(surface);
return handle;
}
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, size_t font_size)
{
memset(&rdp_device, 0, sizeof(RDP_DEVICE));
window = (SDL_Window *)_window;
SDL_SyncWindow(window);
bool result = SDL_AddEventWatch(sdl_event_filter, nullptr);
if (!result)
{
@@ -257,32 +310,51 @@ void rdp_init(void *_window, GFX_INFO _gfx_info)
if (!::Vulkan::Context::init_loader((PFN_vkGetInstanceProcAddr)SDL_Vulkan_GetVkGetInstanceProcAddr()))
{
rdp_close();
return;
}
if (!wsi->init_simple(1, handles))
{
rdp_close();
return;
}
rdp_new_processor(gfx_info);
if (!processor->device_is_supported())
{
delete processor;
delete wsi;
processor = nullptr;
rdp_close();
return;
}
message_font = TTF_OpenFontIO(SDL_IOFromConstMem(font, font_size), true, 30.0);
if (!message_font)
{
rdp_close();
return;
}
wsi->begin_frame();
callback.emu_running = true;
callback.enable_speedlimiter = true;
callback.paused = false;
callback.save_state_slot = 0;
crop_letterbox = false;
messages = std::queue<std::string>();
message_timer = 0;
}
void rdp_close()
{
wsi->end_frame();
if (wsi)
wsi->end_frame();
if (message_font)
{
TTF_CloseFont(message_font);
message_font = nullptr;
}
if (processor)
{
delete processor;
@@ -412,11 +484,30 @@ static void render_frame(Vulkan::Device &device)
cmd->push_constants(&push, 0, sizeof(push));
}
cmd->set_texture(0, 0, image->get_view(), Vulkan::StockSampler::LinearClamp);
cmd->set_texture(0, 0, image->get_view(), Vulkan::StockSampler::NearestClamp);
cmd->set_viewport(vp);
// The vertices are constants in the shader.
// Draws fullscreen quad using oversized triangle.
cmd->draw(3);
if (!messages.empty())
{
Vulkan::ImageHandle message_image = create_message_image(device, vp.width, messages.front().c_str());
cmd->set_texture(0, 0, message_image->get_view(), Vulkan::StockSampler::NearestClamp);
vp.x = vp.x + (vp.width - message_image->get_width()) / 2;
vp.y = vp.y + vp.height - message_image->get_height();
vp.height = message_image->get_height();
vp.width = message_image->get_width();
cmd->set_viewport(vp);
cmd->draw(3);
if (SDL_GetTicks() > message_timer)
{
messages.pop();
message_timer = SDL_GetTicks() + MESSAGE_TIME;
}
}
}
cmd->end_render_pass();
@@ -435,6 +526,7 @@ void rdp_update_screen()
render_frame(device);
wsi->end_frame();
wsi->begin_frame();
rdp_device.vi_counter += 1;
}
CALL_BACK rdp_check_callback()
@@ -462,6 +554,13 @@ void rdp_load_state(const uint8_t *state)
memcpy(&rdp_device, state, sizeof(RDP_DEVICE));
}
void rdp_onscreen_message(const char *_message)
{
if (messages.empty())
message_timer = SDL_GetTicks() + MESSAGE_TIME;
messages.push(_message);
}
uint64_t rdp_process_commands()
{
uint64_t interrupt_timer = 0;
@@ -542,7 +641,11 @@ uint64_t rdp_process_commands()
break;
}
case RDP::Op::SyncFull:
processor->wait_for_timeline(processor->signal_timeline());
if (rdp_device.last_sync != rdp_device.vi_counter)
{
processor->wait_for_timeline(processor->signal_timeline());
rdp_device.last_sync = rdp_device.vi_counter;
}
interrupt_timer = rdp_device.region;
if (interrupt_timer == 0)
interrupt_timer = 5000;
+4 -1
View File
@@ -33,14 +33,17 @@ extern "C"
bool enable_speedlimiter;
bool lower_volume;
bool raise_volume;
bool paused;
uint32_t save_state_slot;
} CALL_BACK;
void rdp_init(void *_window, GFX_INFO _gfx_info);
void rdp_init(void *_window, GFX_INFO _gfx_info, const void *font, size_t font_size);
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_onscreen_message(const char *_message);
void rdp_new_processor(GFX_INFO _gfx_info);
size_t rdp_state_size();
void rdp_save_state(uint8_t *state);
-1
View File
@@ -56,7 +56,6 @@ bool SDL_WSIPlatform::alive(Vulkan::WSI &wsi)
void SDL_WSIPlatform::poll_input()
{
SDL_PumpEvents();
}
void SDL_WSIPlatform::poll_input_async(Granite::InputTrackerHandler *handler)
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.88.0"
channel = "1.89.0"
+189
View File
@@ -0,0 +1,189 @@
use crate::device;
use crate::ui;
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone)]
pub struct DecodedCheat {
code_type: u8,
address: u32,
data: u16,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Cheats {
pub cheats: Vec<Vec<DecodedCheat>>,
pub boot: bool,
pub enabled: bool,
}
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 re = regex::Regex::new(r"\?+").unwrap();
for cheat_setting in cheat_settings.iter() {
if let Some(cheat_data) = cheats.get(cheat_setting.0) {
let mut option_value = None;
if let Some(option) = cheat_setting.1 {
if let Some(found_option_value) = cheat_data
.options
.as_ref()
.cloned()
.unwrap_or_default()
.get(option)
{
option_value = Some(found_option_value.to_string());
} else {
panic!("Cheat option: {option} not found");
}
}
println!(
"Cheat: {} Option: {}",
cheat_setting.0,
cheat_setting.1.clone().unwrap_or("none".to_string())
);
let mut decoded_cheat: Vec<DecodedCheat> = vec![];
for code in cheat_data.data.iter() {
let mut result = code.clone();
if let Some(option_value) = option_value.as_ref() {
result = re.replace_all(code, option_value).into_owned();
}
let mut split = result.split_whitespace();
let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
decoded_cheat.push(DecodedCheat {
code_type: (first_part >> 24) as u8,
address: first_part & 0x00FFFFFF,
data: u16::from_str_radix(split.next().unwrap(), 16).unwrap(),
});
}
device.cheats.cheats.push(decoded_cheat);
} else {
println!("Could not find cheat: {}", cheat_setting.0);
}
}
if !device.cheats.cheats.is_empty() {
device.cheats.enabled = true;
ui::video::onscreen_message(&device.ui, "Cheats enabled");
}
}
fn write_byte(device: &mut device::Device, cheat_line: &DecodedCheat) {
*device
.rdram
.mem
.get_mut(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = cheat_line.data as u8;
}
fn write_half(device: &mut device::Device, cheat_line: &DecodedCheat) {
*device
.rdram
.mem
.get_mut(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = (cheat_line.data >> 8) as u8;
*device
.rdram
.mem
.get_mut((cheat_line.address + 1) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = cheat_line.data as u8;
}
fn equal_byte(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
let current_value = *device
.rdram
.mem
.get(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&0);
current_value == (cheat_line.data as u8)
}
fn equal_half(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
let current_value1 = *device
.rdram
.mem
.get(cheat_line.address as usize ^ device.byte_swap)
.unwrap_or(&0);
let current_value2 = *device
.rdram
.mem
.get((cheat_line.address + 1) as usize ^ device.byte_swap)
.unwrap_or(&0);
current_value1 == (cheat_line.data >> 8) as u8 && current_value2 == cheat_line.data as u8
}
pub fn execute_cheats(device: &mut device::Device, cheats: Vec<Vec<DecodedCheat>>) {
for cheat in cheats.iter() {
let mut valid = true;
let mut cheat_iter = cheat.iter().peekable();
while let Some(cheat_line) = cheat_iter.next() {
match cheat_line.code_type {
0x50 => {
if valid {
let mut expanded_cheat: Vec<DecodedCheat> = vec![];
let compressed_cheat = *cheat_iter.peek().unwrap();
let count = (cheat_line.address & 0xFF00) >> 8;
let offset = cheat_line.address & 0xFF;
for i in 0..count {
let line = DecodedCheat {
code_type: compressed_cheat.code_type,
address: compressed_cheat.address + (i * offset),
data: compressed_cheat.data
+ (i as i16 * cheat_line.data as i16) as u16,
};
expanded_cheat.push(line);
}
execute_cheats(device, [expanded_cheat].to_vec());
}
valid = false; // skip the next line
}
0x80 | 0xA0 => {
if valid {
write_byte(device, cheat_line);
}
valid = true;
}
0x81 | 0xA1 => {
if valid {
write_half(device, cheat_line);
}
valid = true;
}
0xF0 => {
if device.cheats.boot && valid {
write_byte(device, cheat_line);
valid = true;
}
}
0xF1 => {
if device.cheats.boot && valid {
write_half(device, cheat_line);
valid = true;
}
}
0xD0 => {
valid = equal_byte(device, cheat_line);
}
0xD1 => {
valid = equal_half(device, cheat_line);
}
0xD2 => {
valid = !equal_byte(device, cheat_line);
}
0xD3 => {
valid = !equal_half(device, cheat_line);
}
0x88 | 0x89 | 0xD8 | 0xD9 | 0xDA | 0xDB => { /* GS button not emulated */ }
_ => panic!("Unknown cheat code type: {:X}", cheat_line.code_type),
}
}
}
device.cheats.boot = 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 3cf69760cc
File diff suppressed because it is too large Load Diff
+22 -11
View File
@@ -12,7 +12,7 @@ use std::arch::x86_64::*;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use crate::{netplay, ui};
use crate::{cheats, netplay, ui};
use std::{collections::HashMap, fs, io::Read};
pub mod ai;
@@ -44,9 +44,12 @@ pub mod tlb;
pub mod unmapped;
pub mod vi;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, overclock: bool) {
device.ui.video.fullscreen = fullscreen;
device.cpu.overclock = overclock;
pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, game_settings: ui::gui::GameSettings) {
device.ui.video.fullscreen = game_settings.fullscreen;
device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak {
device.rdram.size = 0x400000;
}
init_rng(device);
@@ -55,8 +58,8 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, ov
// rdram pointer is shared with parallel-rdp
rdram::init(device);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::video::init(device);
ui::audio::init(&mut device.ui, device.ai.freq);
ui::input::init(&mut device.ui);
mi::init(device);
@@ -74,12 +77,14 @@ pub fn run_game(device: &mut Device, rom_contents: Vec<u8>, fullscreen: bool, ov
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);
cpu::run(device);
ui::input::close(&mut device.ui);
ui::video::close(&device.ui);
ui::audio::close(&mut device.ui);
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 {
@@ -146,12 +151,12 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Option<Vec<u8>> {
}
} else if file_path.extension().unwrap().eq_ignore_ascii_case("7z") {
let mut archive =
sevenz_rust2::SevenZReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
sevenz_rust2::ArchiveReader::open(file_path, sevenz_rust2::Password::empty()).unwrap();
let mut found = false;
archive
.for_each_entries(
&mut |entry: &sevenz_rust2::SevenZArchiveEntry, reader: &mut dyn std::io::Read| {
&mut |entry: &sevenz_rust2::ArchiveEntry, reader: &mut dyn std::io::Read| {
let name = entry.name().to_ascii_lowercase();
if !found
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
@@ -185,7 +190,7 @@ pub struct Device {
pub netplay: Option<netplay::Netplay>,
#[serde(skip, default = "ui::Ui::default")]
pub ui: ui::Ui,
byte_swap: usize,
pub byte_swap: usize,
pub save_state: bool,
pub load_state: bool,
pub cpu: cpu::Cpu,
@@ -206,6 +211,7 @@ pub struct Device {
pub vru: controller::vru::Vru,
pub vru_window: controller::vru::VruWindow,
pub transferpaks: [controller::transferpak::TransferPak; 4],
pub cheats: cheats::Cheats,
}
pub fn zero_m128i() -> __m128i {
@@ -497,6 +503,11 @@ impl Device {
controller::transferpak::TransferPak::default(),
controller::transferpak::TransferPak::default(),
],
cheats: cheats::Cheats {
cheats: vec![],
boot: true,
enabled: false,
},
}
}
}
+2 -2
View File
@@ -146,8 +146,8 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
AI_DACRATE_REG => {
if device.ai.regs[reg as usize] != value & mask {
device.ai.freq = device.vi.clock / (1 + (value & mask)) as u64;
ui::audio::close(&mut device.ui);
ui::audio::init(&mut device.ui, device.ai.freq)
ui::audio::close_game_audio(&mut device.ui);
ui::audio::init_game_audio(&mut device.ui, device.ai.freq)
}
device::memory::masked_write_32(&mut device.ai.regs[reg as usize], value, mask)
}
-10
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,
+17 -11
View File
@@ -50,9 +50,7 @@ pub fn read_mem(
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0
&& device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] == 0
{
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0 {
let masked_address = address as usize & CART_MASK;
let mut data = read_cart_word(device, masked_address);
device::memory::masked_write_32(&mut data, value, mask);
@@ -82,16 +80,18 @@ pub fn dma_read(
mut dram_addr: u32,
length: u32,
) -> u64 {
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0
&& device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] == 0
{
if device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] != 0 {
dram_addr &= device::rdram::RDRAM_MASK as u32;
cart_addr &= CART_MASK as u32;
for i in 0..length {
device.ui.storage.saves.romsave.data.insert(
cart_addr + i,
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap],
*device
.rdram
.mem
.get((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&0),
);
}
device.ui.storage.saves.romsave.written = true;
@@ -113,10 +113,17 @@ pub fn dma_write(
let mut j = cart_addr;
while i < dram_addr + length {
if let Some(value) = device.ui.storage.saves.romsave.data.get(&j) {
device.rdram.mem[i as usize ^ device.byte_swap] = *value;
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *value;
} else {
device.rdram.mem[i as usize ^ device.byte_swap] =
*device.cart.rom.get(j as usize).unwrap_or(&0);
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *device.cart.rom.get(j as usize).unwrap_or(&0);
}
i += 1;
j += 1;
@@ -182,7 +189,6 @@ fn set_cic(device: &mut device::Device) {
"53C0088FB777870D0AF32F0251E964030E2E8B72E830C26042FD191169508C05" => {
device.cart.cic_seed = 0xdd; // CicNus5167
device.cart.sc64.cfg[device::cart::sc64::SC64_ROM_WRITE_ENABLE as usize] = 1;
device.cart.sc64.cfg[device::cart::sc64::SC64_BOOTLOADER_SWITCH as usize] = 0;
}
_ => {
device.cart.cic_seed = 0x3F; // CicNus6102
+47 -34
View File
@@ -155,24 +155,26 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
let mut i = 0;
while i < length {
if offset + i < device.ui.storage.saves.sdcard.data.len() {
let data = u32::from_be_bytes(
device.ui.storage.saves.sdcard.data
[(offset + i)..(offset + i + 4)]
.try_into()
.unwrap(),
);
let data = u32::from_be_bytes(
device
.ui
.storage
.saves
.sdcard
.data
.get((offset + i)..(offset + i + 4))
.unwrap_or(&[0; 4])
.try_into()
.unwrap_or_default(),
);
device::memory::data_write(
device,
address + i as u64,
data,
0xFFFFFFFF,
false,
);
} else {
panic!("sd card read out of bounds")
}
device::memory::data_write(
device,
address + i as u64,
data,
0xFFFFFFFF,
false,
);
i += 4;
}
}
@@ -187,19 +189,22 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
let mut i = 0;
while i < length {
if offset + i < device.ui.storage.saves.sdcard.data.len() {
let data = device::memory::data_read(
device,
address + i as u64,
device::memory::AccessSize::Word,
false,
)
.to_be_bytes();
device.ui.storage.saves.sdcard.data[(offset + i)..(offset + i + 4)]
.copy_from_slice(&data);
} else {
panic!("sd card write out of bounds")
}
let data = device::memory::data_read(
device,
address + i as u64,
device::memory::AccessSize::Word,
false,
)
.to_be_bytes();
device
.ui
.storage
.saves
.sdcard
.data
.get_mut((offset + i)..(offset + i + 4))
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data);
i += 4;
}
device.ui.storage.saves.sdcard.written = true;
@@ -308,8 +313,12 @@ pub fn dma_read(
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
buffer[j as usize] = device.rdram.mem[i as usize ^ device.byte_swap];
while i < dram_addr + length {
*buffer.get_mut(j as usize).unwrap_or(&mut 0) = *device
.rdram
.mem
.get(i as usize ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
j += 1;
}
@@ -335,8 +344,12 @@ pub fn dma_write(
let mut i = dram_addr;
let mut j = cart_addr;
while i < dram_addr + length && i < device.rdram.size {
device.rdram.mem[i as usize ^ device.byte_swap] = buffer[j as usize];
while i < dram_addr + length {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *buffer.get(j as usize).unwrap_or(&0);
i += 1;
j += 1;
}
+46 -12
View File
@@ -150,9 +150,19 @@ fn dma_read_sram(device: &mut device::Device, mut cart_addr: u32, mut dram_addr:
format_sram(device);
while i < dram_addr + length && i < device.rdram.size {
device.ui.storage.saves.sram.data[j as usize] =
device.rdram.mem[i as usize ^ device.byte_swap];
while i < dram_addr + length {
*device
.ui
.storage
.saves
.sram
.data
.get_mut(j as usize)
.unwrap_or(&mut 0) = *device
.rdram
.mem
.get(i as usize ^ device.byte_swap)
.unwrap_or(&0);
i += 1;
j += 1;
}
@@ -169,8 +179,11 @@ fn dma_read_flash(device: &mut device::Device, cart_addr: u32, dram_addr: u32, l
{
/* load page buf using DMA */
for i in 0..length {
device.cart.flashram.page_buf[i as usize] =
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap];
device.cart.flashram.page_buf[i as usize] = *device
.rdram
.mem
.get((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&0);
}
} else {
/* other accesses are not implemented */
@@ -206,9 +219,19 @@ fn dma_write_sram(
format_sram(device);
while i < dram_addr + length && i < device.rdram.size {
device.rdram.mem[i as usize ^ device.byte_swap] =
device.ui.storage.saves.sram.data[j as usize];
while i < dram_addr + length {
*device
.rdram
.mem
.get_mut(i as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = *device
.ui
.storage
.saves
.sram
.data
.get(j as usize)
.unwrap_or(&0);
i += 1;
j += 1;
}
@@ -227,10 +250,18 @@ fn dma_write_flash(
&& device.cart.flashram.mode == FlashramMode::ReadSiliconId
{
/* read Silicon ID using DMA */
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.cart.flashram.silicon_id[0].to_ne_bytes());
dram_addr += 4;
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.cart.flashram.silicon_id[1].to_ne_bytes());
} else if (cart_addr & 0x1ffff) < 0x10000
&& device.cart.flashram.mode == FlashramMode::ReadArray
@@ -250,8 +281,11 @@ fn dma_write_flash(
/* do actual DMA */
for i in 0..length {
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap] =
device.ui.storage.saves.flash.data[(cart_addr + i) as usize];
*device
.rdram
.mem
.get_mut((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = device.ui.storage.saves.flash.data[(cart_addr + i) as usize];
}
} else {
/* other accesses are not implemented */
+3 -1
View File
@@ -189,20 +189,22 @@ pub fn pak_switch_event(device: &mut device::Device) {
write: device::controller::mempak::write,
pak_type: new_pak_type,
});
ui::video::onscreen_message(&device.ui, "MemPak selected");
} 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(&device.ui, "RumblePak selected");
} 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(&device.ui, "TransferPak selected");
}
ui::audio::play_pak_switch(&device.ui, new_pak_type);
channel.change_pak = PakType::None;
}
}
+12
View File
@@ -2726,5 +2726,17 @@ fn create_word_mappings(device: &mut device::Device) {
String::from("82B582E382C182CF82C282AF82A282A982A2"),
String::from("restricted-depart"),
),
(
String::from("009903720366006903C903FF0045033C"),
String::from("objection"),
),
(
String::from("041100CF02EE0381004503B7"),
String::from("hold-it"),
),
(
String::from("03BA019203C9036C0087000303C6"),
String::from("take-that"),
),
]);
}
+5 -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,10 @@ 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(
&device.ui,
"Overclocking enabled, setting clock rate to 125 MHz",
);
125000000
};
+5 -1
View File
@@ -221,7 +221,11 @@ fn unknown_dma_write(
dram_addr &= device::rdram::RDRAM_MASK as u32;
for i in 0..length {
device.rdram.mem[(dram_addr + i) as usize ^ device.byte_swap] = 0;
*device
.rdram
.mem
.get_mut((dram_addr + i) as usize ^ device.byte_swap)
.unwrap_or(&mut 0) = 0;
}
device::pi::calculate_cycles(device, 1, length)
}
+44 -23
View File
@@ -29,9 +29,13 @@ pub fn read_mem_fast(
) -> u32 {
let masked_address = address as usize & RDRAM_MASK;
u32::from_ne_bytes(
device.rdram.mem[masked_address..masked_address + 4]
device
.rdram
.mem
.get(masked_address..masked_address + 4)
.unwrap_or(&[0; 4])
.try_into()
.unwrap(),
.unwrap_or_default(),
)
}
@@ -45,28 +49,35 @@ pub fn read_mem(
rdram_calculate_cycles(access_size as u64) / (access_size as u64 / 4),
);
let masked_address = address as usize & RDRAM_MASK;
if address < device.rdram.size as u64 {
u32::from_ne_bytes(
device.rdram.mem[masked_address..masked_address + 4]
.try_into()
.unwrap(),
)
} else {
0
}
u32::from_ne_bytes(
device
.rdram
.mem
.get(masked_address..masked_address + 4)
.unwrap_or(&[0; 4])
.try_into()
.unwrap_or_default(),
)
}
pub fn write_mem(device: &mut device::Device, address: u64, value: u32, mask: u32) {
if address < device.rdram.size as u64 {
let mut data = u32::from_ne_bytes(
device.rdram.mem[address as usize..(address + 4) as usize]
.try_into()
.unwrap(),
);
device::memory::masked_write_32(&mut data, value, mask);
device.rdram.mem[address as usize..(address + 4) as usize]
.copy_from_slice(&data.to_ne_bytes());
}
let mut data = u32::from_ne_bytes(
device
.rdram
.mem
.get(address as usize..(address + 4) as usize)
.unwrap_or(&[0; 4])
.try_into()
.unwrap_or_default(),
);
device::memory::masked_write_32(&mut data, value, mask);
device
.rdram
.mem
.get_mut(address as usize..(address + 4) as usize)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
}
pub fn read_regs(
@@ -103,9 +114,19 @@ pub fn init(device: &mut device::Device) {
unsafe { Vec::from_raw_parts(ptr, device.rdram.size as usize, device.rdram.size as usize) };
// hack, skip RDRAM initialization
device.rdram.mem[0x318..0x318 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
device
.rdram
.mem
.get_mut(0x318..0x318 + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.rdram.size.to_ne_bytes());
// hack, skip RDRAM initialization
device.rdram.mem[0x3f0..0x3f0 + 4].copy_from_slice(&device.rdram.size.to_ne_bytes());
device
.rdram
.mem
.get_mut(0x3f0..0x3f0 + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&device.rdram.size.to_ne_bytes());
device.ri.regs[device::ri::RI_MODE_REG as usize] = 0x0e;
device.ri.regs[device::ri::RI_CONFIG_REG as usize] = 0x40;
+15 -9
View File
@@ -157,7 +157,11 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
.try_into()
.unwrap(),
);
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
device
.rdram
.mem
.get_mut(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
mem_addr += 4;
dram_addr += 4;
@@ -171,14 +175,16 @@ fn do_dma(device: &mut device::Device, dma: RspDma) {
while j < count {
let mut i = 0;
while i < length {
let mut data = 0;
if dram_addr < device.rdram.size {
data = u32::from_ne_bytes(
device.rdram.mem[dram_addr as usize..dram_addr as usize + 4]
.try_into()
.unwrap(),
);
}
let data = u32::from_ne_bytes(
device
.rdram
.mem
.get(dram_addr as usize..dram_addr as usize + 4)
.unwrap_or(&[0; 4])
.try_into()
.unwrap_or_default(),
);
if offset != 0 {
// imem being updated
device.rsp.cpu.instructions[((mem_addr & 0xFFF) / 4) as usize].func =
+8 -8
View File
@@ -709,7 +709,7 @@ pub fn lsv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 2 == 0 {
if element.is_multiple_of(2) {
modify_vpr16(
&mut device.rsp.cpu.vpr[rt(opcode) as usize],
element / 2,
@@ -736,7 +736,7 @@ pub fn llv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 4 == 0 {
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));
@@ -766,7 +766,7 @@ pub fn ldv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 8 == 0 {
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));
@@ -796,7 +796,7 @@ pub fn lqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address % 16 == 0 {
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));
@@ -975,7 +975,7 @@ pub fn ssv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 2 == 0 {
if element.is_multiple_of(2) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 2].copy_from_slice(
&get_vpr16(device.rsp.cpu.vpr[rt(opcode) as usize], element / 2).to_be_bytes(),
@@ -997,7 +997,7 @@ pub fn slv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 4 == 0 {
if element.is_multiple_of(4) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 4].copy_from_slice(
&get_vpr32(device.rsp.cpu.vpr[rt(opcode) as usize], element / 4).to_be_bytes(),
@@ -1019,7 +1019,7 @@ pub fn sdv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element % 8 == 0 {
if element.is_multiple_of(8) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 8].copy_from_slice(
&get_vpr64(device.rsp.cpu.vpr[rt(opcode) as usize], element / 8).to_be_bytes(),
@@ -1041,7 +1041,7 @@ pub fn sqv(device: &mut device::Device, opcode: u32) {
let mut element = velement(opcode);
if element == 0 && address % 16 == 0 {
if element == 0 && address.is_multiple_of(16) {
let start = (address & 0xFFF) as usize;
device.rsp.mem[start..start + 16]
.copy_from_slice(&get_vpr128(device.rsp.cpu.vpr[rt(opcode) as usize]).to_be_bytes());
+12 -3
View File
@@ -86,9 +86,13 @@ fn copy_pif_rdram(device: &mut device::Device) {
let mut i = 0;
while i < device::pif::PIF_RAM_SIZE {
let data = u32::from_ne_bytes(
device.rdram.mem[dram_addr + i..dram_addr + i + 4]
device
.rdram
.mem
.get(dram_addr + i..dram_addr + i + 4)
.unwrap_or(&[0; 4])
.try_into()
.unwrap(),
.unwrap_or_default(),
);
device.pif.ram[i..i + 4].copy_from_slice(&data.to_be_bytes());
i += 4;
@@ -97,7 +101,12 @@ fn copy_pif_rdram(device: &mut device::Device) {
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());
device.rdram.mem[dram_addr + i..dram_addr + i + 4].copy_from_slice(&data.to_ne_bytes());
device
.rdram
.mem
.get_mut(dram_addr + i..dram_addr + i + 4)
.unwrap_or(&mut [0; 4])
.copy_from_slice(&data.to_ne_bytes());
i += 4;
}
} else {
+23 -14
View File
@@ -1,4 +1,4 @@
use crate::{device, netplay, ui};
use crate::{cheats, device, netplay, ui};
use governor::clock::Clock;
const VI_STATUS_REG: u32 = 0;
@@ -134,16 +134,22 @@ pub fn write_regs(device: &mut device::Device, address: u64, value: u32, mask: u
}
pub fn vertical_interrupt_event(device: &mut device::Device) {
ui::video::check_callback(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);
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
if device.vi.enable_speed_limiter == device.netplay.as_ref().unwrap().fast_forward {
speed_limiter_toggled = true;
device.vi.enable_speed_limiter = !device.netplay.as_ref().unwrap().fast_forward;
}
}
device.vi.vi_counter += 1;
if device.vi.vi_counter % device.vi.limit_freq == 0 && device.vi.enable_speed_limiter {
speed_limiter(device);
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();
@@ -175,7 +181,7 @@ pub fn init(device: &mut device::Device) {
}
}
fn speed_limiter(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());
@@ -194,14 +200,17 @@ fn speed_limiter(device: &mut device::Device) {
.as_secs_f64()
> 1.0
{
if device.vi.min_wait_time.as_secs_f64() == 0.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
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
if !speed_limiter_toggled {
if device.vi.min_wait_time.as_secs_f64() == 0.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
&& device.vi.limit_freq > 1
{
device.vi.limit_freq -= 1;
create_limiter(device);
}
}
//println!("limit freq: {}", device.vi.limit_freq);
+26 -11
View File
@@ -3,6 +3,7 @@
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod savestates;
@@ -106,6 +107,14 @@ async fn main() {
}
return;
}
if args.configure_input_profile.is_some() {
ui::input::configure_input_profile(
&mut ui,
args.configure_input_profile.unwrap(),
args.use_dinput,
);
return;
}
if args.assign_controller.is_some() {
if args.port.is_none() {
println!("Must specify port number");
@@ -116,7 +125,6 @@ async fn main() {
args.assign_controller.unwrap(),
args.port.unwrap(),
);
return;
}
if args.bind_input_profile.is_some() {
if args.port.is_none() {
@@ -128,15 +136,6 @@ async fn main() {
args.bind_input_profile.unwrap(),
args.port.unwrap(),
);
return;
}
if args.configure_input_profile.is_some() {
ui::input::configure_input_profile(
&mut ui,
args.configure_input_profile.unwrap(),
args.use_dinput,
);
return;
}
} else if args.game.is_some() {
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
@@ -147,7 +146,23 @@ async fn main() {
.spawn(move || {
let mut device = device::Device::new();
let overclock = device.ui.config.emulation.overclock;
device::run_game(&mut device, rom_contents, args.fullscreen, 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();
+6 -16
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;
@@ -113,7 +105,7 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
pub fn send_sync_check(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
if netplay.vi_counter % 600 == 0 {
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());
@@ -155,7 +147,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(&device.ui, "Lost connection to netplay server");
input = Some(InputEvent {
input: 0,
plugin: 0,
@@ -200,15 +192,13 @@ 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(ui, "Netplay desync detected");
ui::video::onscreen_message(ui, "Netplay desync detected"); // queue this message twice to ensure they see it
}
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(ui, &format!("Player {dis} disconnected"));
}
}
netplay.status = current_status;
@@ -252,7 +242,7 @@ pub fn init(peer_addr: std::net::SocketAddr, player_number: u8) -> Netplay {
udp_socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.expect("couldn't bind to address");
socket2::SockRef::from(&udp_socket)
.set_tos(CS4 << 2)
.set_tos_v4(CS4 << 2)
.unwrap();
} else {
udp_socket = std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0))
+22 -9
View File
@@ -87,9 +87,12 @@ pub fn create_savestate(device: &device::Device) {
compressed_file,
)
.unwrap();
println!(
"Savestate created at {}",
device.ui.storage.paths.savestate_file_path.display()
ui::video::onscreen_message(
&device.ui,
&format!(
"Savestate created in slot {}",
device.ui.storage.save_state_slot
),
);
}
@@ -120,6 +123,7 @@ pub fn load_savestate(device: &mut device::Device) {
device.si = state.si;
device.ri = state.ri;
device.vru = state.vru;
device.cheats = state.cheats;
let mut tpak_rom = [vec![], vec![], vec![], vec![]];
for (i, item) in tpak_rom.iter_mut().enumerate() {
@@ -191,15 +195,24 @@ pub fn load_savestate(device: &mut device::Device) {
}
}
ui::audio::close(&mut device.ui);
ui::audio::init(&mut device.ui, device.ai.freq);
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()
ui::video::onscreen_message(
&device.ui,
&format!(
"Savestate loaded from slot {}",
device.ui.storage.save_state_slot
),
);
} else {
println!("Failed to load savestate");
ui::video::onscreen_message(
&device.ui,
&format!(
"Failed to load savestate from slot {}",
device.ui.storage.save_state_slot
),
);
}
}
}
+14 -17
View File
@@ -1,4 +1,5 @@
pub mod audio;
pub mod cheats;
pub mod config;
pub mod gui;
pub mod input;
@@ -13,10 +14,9 @@ pub struct Dirs {
}
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,
}
@@ -30,6 +30,7 @@ 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 {
@@ -53,6 +54,7 @@ impl Drop for Ui {
fn drop(&mut self) {
if self.with_sdl {
unsafe {
sdl3_ttf_sys::ttf::TTF_Quit();
sdl3_sys::init::SDL_Quit();
}
}
@@ -71,6 +73,14 @@ pub fn sdl_init(flag: sdl3_sys::init::SDL_InitFlags) {
}
}
pub fn ttf_init() {
unsafe {
if !sdl3_ttf_sys::ttf::TTF_Init() {
panic!("Could not initialize TTF");
}
}
}
pub fn get_dirs() -> Dirs {
let exe_path = std::env::current_exe().unwrap();
let portable_dir = exe_path.parent();
@@ -123,6 +133,7 @@ impl Ui {
joysticks,
},
storage: Storage {
save_state_slot: 0,
save_type: vec![],
paths: storage::Paths {
eep_file_path: std::path::PathBuf::new(),
@@ -165,22 +176,8 @@ 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(),
],
},
audio_device_spec: Default::default(),
audio_stream: std::ptr::null_mut(),
event_audio_stream: std::ptr::null_mut(),
audio_device: 0,
gain: 1.0,
},
+26 -103
View File
@@ -1,24 +1,9 @@
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 fn init(ui: &mut ui::Ui, frequency: u64) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_AUDIO);
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
channels: 2,
};
let device_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: 48000,
@@ -34,19 +19,29 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
panic!("Could not open audio device");
}
let mut dst = Default::default();
if !unsafe {
sdl3_sys::audio::SDL_GetAudioDeviceFormat(
ui.audio.audio_device,
&mut dst,
&mut ui.audio.audio_device_spec,
std::ptr::null_mut(),
)
} {
panic!("Could not get audio device format");
}
ui.audio.audio_stream =
unsafe { sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &dst) };
init_game_audio(ui, frequency);
}
pub fn init_game_audio(ui: &mut ui::Ui, frequency: u64) {
let game_audio_spec = sdl3_sys::audio::SDL_AudioSpec {
format: sdl3_sys::audio::SDL_AUDIO_S16LE,
freq: frequency as i32,
channels: 2,
};
ui.audio.audio_stream = unsafe {
sdl3_sys::audio::SDL_CreateAudioStream(&game_audio_spec, &ui.audio.audio_device_spec)
};
if ui.audio.audio_stream.is_null() {
return;
}
@@ -56,38 +51,22 @@ pub fn init(ui: &mut ui::Ui, frequency: u64) {
} {
panic!("Could not bind audio stream");
}
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, &dst) };
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");
}
}
pub fn close(ui: &mut ui::Ui) {
close_game_audio(ui);
unsafe {
sdl3_sys::audio::SDL_CloseAudioDevice(ui.audio.audio_device);
ui.audio.audio_device = 0;
}
}
pub fn close_game_audio(ui: &mut ui::Ui) {
unsafe {
if !ui.audio.audio_stream.is_null() {
sdl3_sys::audio::SDL_DestroyAudioStream(ui.audio.audio_stream);
ui.audio.audio_stream = std::ptr::null_mut();
}
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;
}
}
@@ -98,7 +77,6 @@ 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);
}
}
@@ -109,61 +87,6 @@ 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);
}
}
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() {
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");
}
}
@@ -176,12 +99,12 @@ pub fn play_audio(device: &device::Device, dram_addr: usize, length: u64) {
let mut i = 0;
while i < length as usize / 2 {
// Left channel
primary_buffer[i] = device.rdram.mem[dram_addr + (i * 2) + 2] as i16
| ((device.rdram.mem[dram_addr + (i * 2) + 3] as i16) << 8);
primary_buffer[i] = *device.rdram.mem.get(dram_addr + (i * 2) + 2).unwrap_or(&0) as i16
| ((*device.rdram.mem.get(dram_addr + (i * 2) + 3).unwrap_or(&0) as i16) << 8);
// Right channel
primary_buffer[i + 1] = device.rdram.mem[dram_addr + (i * 2)] as i16
| ((device.rdram.mem[dram_addr + (i * 2) + 1] as i16) << 8);
primary_buffer[i + 1] = *device.rdram.mem.get(dram_addr + (i * 2)).unwrap_or(&0) as i16
| ((*device.rdram.mem.get(dram_addr + (i * 2) + 1).unwrap_or(&0) as i16) << 8);
i += 2;
}
+156
View File
@@ -0,0 +1,156 @@
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| {
let select_rom = if !rom_dir.is_empty()
&& let Ok(exists) = std::fs::exists(&rom_dir)
&& exists
{
rfd::AsyncFileDialog::new().set_directory(rom_dir)
} else {
rfd::AsyncFileDialog::new()
}
.set_title("Select ROM")
.add_filter("ROM files", &ui::gui::N64_EXTENSIONS)
.pick_file();
let weak = weak.clone();
tokio::spawn(async move {
if let Some(file) = select_rom.await
&& let Some(rom_contents) = device::get_rom_contents(file.path())
{
let game_crc = ui::storage::get_game_crc(&rom_contents);
let game_name = ui::storage::get_game_name(&rom_contents);
weak.upgrade_in_event_loop(move |handle| {
handle.set_cheat_game_name(game_name.into());
})
.unwrap();
let cheats: Cheats =
serde_json::from_slice(include_bytes!("../../data/cheats.json")).unwrap();
if let Some(cheat) = cheats.get(&game_crc) {
let cheat = cheat.clone();
weak.upgrade_in_event_loop(move |handle| {
let cheat_settings = ui::config::Cheats::new();
let game_cheats = cheat_settings.cheats.get(&game_crc).cloned();
handle.set_cheat_game_crc(game_crc.into());
let cheats_vec = slint::VecModel::default();
for item in cheat.iter() {
let mut cheat_enabled = false;
if let Some(game_cheat) = game_cheats.as_ref()
&& game_cheat.contains_key(item.0)
{
cheat_enabled = true;
}
let options_vec = slint::VecModel::default();
if let Some(options) = item.1.options.as_ref() {
for option in options.iter() {
let mut option_enabled = false;
if let Some(game_cheat) = game_cheats.as_ref()
&& game_cheat.contains_key(item.0)
&& let Some(opt) = game_cheat.get(item.0)
&& let Some(opt) = opt
&& opt == option.0
{
option_enabled = true;
}
options_vec.push((option_enabled, option.0.into()));
}
}
let options = slint::ModelRc::from(std::rc::Rc::new(options_vec));
cheats_vec.push((
item.0.clone().into(),
cheat_enabled,
item.1.clone().note.into(),
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));
})
.unwrap();
} else {
clear_cheats(&weak, false);
}
} else {
clear_cheats(&weak, true);
weak.upgrade_in_event_loop(move |_handle| {
let message_dialog = ErrorDialog::new().unwrap();
let weak_dialog = message_dialog.as_weak();
message_dialog.on_close_clicked(move || {
weak_dialog.unwrap().window().hide().unwrap();
});
message_dialog.set_text("Could not read ROM".into());
message_dialog.show().unwrap();
})
.unwrap();
}
});
});
let weak = app.as_weak();
app.on_cheats_clear_clicked(move || {
let mut cheats = ui::config::Cheats::new();
cheats.cheats.clear();
clear_cheats(&weak, true);
});
app.on_cheat_toggled(move |game_crc, cheat_name, option, enabled| {
let mut cheats = ui::config::Cheats::new();
let mut game_cheats = cheats
.cheats
.remove(&game_crc.to_string())
.unwrap_or_default();
if enabled {
let cheat_option = if option.is_empty() {
None
} else {
Some(option.into())
};
game_cheats.insert(cheat_name.into(), cheat_option);
} else {
game_cheats.remove(&cheat_name.to_string());
}
cheats.cheats.insert(game_crc.into(), game_cheats);
});
}
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));
if clear_name {
handle.set_cheat_game_name("".into());
}
})
.unwrap();
}
+45 -3
View File
@@ -33,7 +33,7 @@ pub struct InputProfile {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Input {
pub input_profiles: std::collections::HashMap<String, InputProfile>,
pub input_profiles: std::collections::BTreeMap<String, InputProfile>,
pub input_profile_binding: [String; 4],
pub controller_assignment: [Option<String>; 4],
pub controller_enabled: [bool; 4],
@@ -53,6 +53,13 @@ pub struct Video {
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Emulation {
pub overclock: bool,
pub disable_expansion_pak: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Cheats {
pub cheats:
std::collections::HashMap<String, std::collections::HashMap<String, Option<String>>>,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -60,6 +67,37 @@ pub struct Config {
pub input: Input,
pub video: Video,
pub emulation: Emulation,
pub rom_dir: std::path::PathBuf,
}
impl Drop for Cheats {
fn drop(&mut self) {
write_cheats(self);
}
}
fn write_cheats(cheats: &Cheats) {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("cheats.json");
let f = std::fs::File::create(file_path).unwrap();
serde_json::to_writer_pretty(f, &cheats).unwrap();
}
impl Cheats {
pub fn new() -> Cheats {
let dirs = ui::get_dirs();
let file_path = dirs.config_dir.join("cheats.json");
let cheats_file = std::fs::read(file_path);
if let Ok(cheats_file) = cheats_file {
let result = serde_json::from_slice(cheats_file.as_ref());
if let Ok(result) = result {
return result;
}
}
Cheats {
cheats: std::collections::HashMap::new(),
}
}
}
impl Drop for Config {
@@ -86,7 +124,7 @@ impl Config {
return result;
}
}
let mut input_profiles = std::collections::HashMap::new();
let mut input_profiles = std::collections::BTreeMap::new();
input_profiles.insert("default".to_string(), ui::input::get_default_profile());
Config {
input: Input {
@@ -109,7 +147,11 @@ impl Config {
widescreen: false,
crt: false,
},
emulation: Emulation { overclock: false },
emulation: Emulation {
overclock: false,
disable_expansion_pak: false,
},
rom_dir: std::path::PathBuf::new(),
}
}
}
+66 -49
View File
@@ -5,6 +5,8 @@ use slint::Model;
slint::include_modules!();
pub const N64_EXTENSIONS: [&str; 5] = ["n64", "v64", "z64", "7z", "zip"];
#[derive(serde::Deserialize)]
struct GithubData {
tag_name: String,
@@ -25,6 +27,14 @@ pub struct VruChannel {
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>>,
}
fn check_latest_version(weak: slint::Weak<AppWindow>) {
let client = reqwest::Client::builder()
.user_agent(env!("CARGO_PKG_NAME"))
@@ -52,43 +62,6 @@ fn check_latest_version(weak: slint::Weak<AppWindow>) {
});
}
fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let weak_create = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_create = controller_paths.to_owned();
app.on_create_session_button_clicked(move || {
let controller_paths = controller_paths_create.clone();
let weak_app = weak_app.clone();
weak_create
.upgrade_in_event_loop(move |handle| {
let create_window = NetplayCreate::new().unwrap();
save_settings(&handle, &controller_paths);
ui::netplay::setup_create_window(
&create_window,
handle.get_overclock_n64_cpu(),
handle.get_fullscreen(),
weak_app,
);
})
.unwrap();
});
let weak_join = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_join = controller_paths.to_owned();
app.on_join_session_button_clicked(move || {
let controller_paths = controller_paths_join.clone();
let weak_app = weak_app.clone();
weak_join
.upgrade_in_event_loop(move |handle| {
let join_window = NetplayJoin::new().unwrap();
save_settings(&handle, &controller_paths);
ui::netplay::setup_join_window(&join_window, handle.get_fullscreen(), weak_app);
})
.unwrap();
});
}
fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let dirs = ui::get_dirs();
let weak = app.as_weak();
@@ -122,6 +95,7 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
app.set_widescreen(config.video.widescreen);
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);
let combobox_value = match config.video.upscale {
1 => 0,
2 => 1,
@@ -129,6 +103,10 @@ fn settings_window(app: &AppWindow, config: &ui::config::Config) {
_ => 0,
};
app.set_resolution(combobox_value);
if let Some(rom_dir_str) = config.rom_dir.to_str() {
app.set_rom_dir(rom_dir_str.into());
}
}
fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Config) {
@@ -244,13 +222,14 @@ fn controller_window(
});
}
fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
let mut config = ui::config::Config::new();
config.video.integer_scaling = app.get_integer_scaling();
config.video.fullscreen = app.get_fullscreen();
config.video.widescreen = app.get_widescreen();
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.video.upscale = upscale_values[app.get_resolution() as usize];
@@ -282,6 +261,15 @@ fn about_window(app: &AppWindow) {
app.on_discord_button_clicked(move || {
open::that_detached("https://discord.gg/9RGXq8W8JQ").unwrap();
});
app.on_patreon_button_clicked(move || {
open::that_detached("https://patreon.com/loganmc10").unwrap();
});
app.on_github_sponsors_button_clicked(move || {
open::that_detached("https://github.com/sponsors/loganmc10").unwrap();
});
app.on_source_code_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64").unwrap();
});
app.on_newversion_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
});
@@ -303,7 +291,8 @@ pub fn app_window() {
controller_window(&app, &game_ui.config, &controller_names, &controller_paths);
}
local_game_window(&app, &controller_paths);
netplay_window(&app, &controller_paths);
ui::netplay::netplay_window(&app, &controller_paths);
ui::cheats::cheats_window(&app);
app.run().unwrap();
save_settings(&app, &controller_paths);
}
@@ -355,8 +344,7 @@ fn setup_vru_word_watcher(
pub fn run_rom(
gb_paths: GbPaths,
file_path: std::path::PathBuf,
fullscreen: bool,
overclock: bool,
mut game_settings: GameSettings,
vru_channel: VruChannel,
netplay: Option<NetplayDevice>,
weak: slint::Weak<AppWindow>,
@@ -369,6 +357,7 @@ pub fn run_rom(
.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() {
@@ -389,8 +378,14 @@ pub fn run_rom(
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, fullscreen, overclock);
device::run_game(&mut device, rom_contents, game_settings);
if device.netplay.is_some() {
netplay::close(&mut device);
}
@@ -402,16 +397,31 @@ pub fn run_rom(
vru_window_notifier.try_send(None).unwrap();
}
weak.upgrade_in_event_loop(move |handle| handle.set_game_running(false))
.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);
})
.unwrap();
})
.unwrap();
}
fn open_rom(app: &AppWindow) {
let select_rom = rfd::AsyncFileDialog::new()
.set_title("Select ROM")
.pick_file();
let rom_dir = app.get_rom_dir();
let select_rom = if !rom_dir.is_empty()
&& let Ok(exists) = std::fs::exists(&rom_dir)
&& exists
{
rfd::AsyncFileDialog::new().set_directory(rom_dir)
} else {
rfd::AsyncFileDialog::new()
}
.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];
@@ -420,11 +430,13 @@ fn open_rom(app: &AppWindow) {
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(),
);
}
@@ -444,6 +456,7 @@ fn open_rom(app: &AppWindow) {
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();
if emulate_vru {
setup_vru_word_watcher(app.as_weak(), vru_word_notifier, vru_window_receiver);
@@ -470,8 +483,12 @@ fn open_rom(app: &AppWindow) {
ram: gb_ram_path,
},
file.path().to_path_buf(),
fullscreen,
overclock,
GameSettings {
fullscreen,
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),
+48 -8
View File
@@ -9,10 +9,27 @@ export global AboutData {
in property <bool> has_update;
}
export component NewVersionButton {
callback newversion_button_clicked;
HorizontalBox {
alignment: center;
Button {
text: @tr("New version available! Click here to download");
primary: true;
clicked => {
newversion_button_clicked();
}
}
}
}
export component About inherits Page {
callback wiki_button_clicked;
callback discord_button_clicked;
callback newversion_button_clicked;
callback patreon_button_clicked;
callback github_sponsors_button_clicked;
callback source_code_button_clicked;
title: @tr("About");
VerticalBox {
@@ -37,6 +54,33 @@ export component About inherits Page {
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Source Code");
clicked => {
source_code_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Donate (via Patreon)");
clicked => {
patreon_button_clicked();
}
}
Button {
text: @tr("Donate (via GitHub)");
clicked => {
github_sponsors_button_clicked();
}
}
}
HorizontalBox {
alignment: center;
Text {
@@ -44,15 +88,11 @@ export component About inherits Page {
}
}
if AboutData.has_update:HorizontalBox {
alignment: center;
Button {
text: @tr("New version available! Click to download");
primary: true;
clicked => {
newversion_button_clicked();
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+63 -5
View File
@@ -1,28 +1,39 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { LocalGame, ControllerConfig, Netplay, Settings, About } from "pages.slint";
import { StandardButton } from "std-widgets.slint";
import { LocalGame, ControllerConfig, Netplay, Cheats, Settings, About } from "pages.slint";
import { SideBar } from "side_bar.slint";
import { SettingsData } from "settings_page.slint";
import { AboutData } from "about_page.slint";
import { ControllerData } from "controller_page.slint";
import { State } from "localgame_page.slint";
import { CheatsData } from "cheat_page.slint";
export { InputProfileDialog } from "input_profile.slint";
export { VruDialog } from "vru_dialog.slint";
export { NetplayCreate } from "netplay_create.slint";
export { NetplayJoin } from "netplay_join.slint";
export { NetplayWait } from "netplay_wait.slint";
export { NetplayDialog } from "netplay_page.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 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 create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
callback netplay_feedback_button_clicked;
callback cheats_select_rom_clicked(string);
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
in property version <=> AboutData.version;
in property has_update <=> AboutData.has_update;
in-out property integer_scaling <=> SettingsData.integer_scaling;
@@ -30,7 +41,9 @@ export component AppWindow inherits Window {
in-out property widescreen <=> SettingsData.widescreen;
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 rom_dir <=> SettingsData.rom_dir;
in-out property emulate_vru <=> ControllerData.emulate_vru;
in-out property controller_enabled <=> ControllerData.controller_enabled;
in-out property transferpak <=> ControllerData.transferpak;
@@ -40,10 +53,13 @@ export component AppWindow inherits Window {
in-out property selected_controller <=> ControllerData.selected_controller;
in-out property game_running <=> State.game_running;
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;
in-out property cheats <=> CheatsData.cheats;
preferred-width: 700px;
preferred-height: 500px;
title: @tr("Gopher64");
icon: @image-url("../../../data/gopher64.png");
icon: @image-url("../../../data/icon/gopher64.png");
HorizontalLayout {
side-bar := SideBar {
@@ -52,6 +68,7 @@ export component AppWindow inherits Window {
@tr("Menu" => "Local Game"),
@tr("Menu" => "Controller Config"),
@tr("Menu" => "Netplay"),
@tr("Menu" => "Cheats"),
@tr("Menu" => "Settings"),
@tr("Menu" => "About")
];
@@ -64,6 +81,9 @@ export component AppWindow inherits Window {
saves_folder_button_clicked => {
saves_folder_button_clicked();
}
newversion_button_clicked => {
newversion_button_clicked();
}
}
if(side-bar.current-item == 1): ControllerConfig {
input_profile_button_clicked => {
@@ -77,9 +97,26 @@ export component AppWindow inherits Window {
join_session_button_clicked => {
join_session_button_clicked();
}
netplay_discord_button_clicked => {
netplay_discord_button_clicked();
}
netplay_feedback_button_clicked => {
netplay_feedback_button_clicked();
}
}
if(side-bar.current-item == 3): Settings { }
if(side-bar.current-item == 4): About {
if(side-bar.current-item == 3): Cheats {
cheats_select_rom_clicked(rom_dir) => {
cheats_select_rom_clicked(rom_dir);
}
cheats_clear_clicked => {
cheats_clear_clicked();
}
cheat_toggled(game_crc, cheat_name, option, enabled) => {
cheat_toggled(game_crc,cheat_name,option,enabled);
}
}
if(side-bar.current-item == 4): Settings { }
if(side-bar.current-item == 5): About {
wiki_button_clicked => {
wiki_button_clicked();
}
@@ -89,6 +126,27 @@ export component AppWindow inherits Window {
newversion_button_clicked => {
newversion_button_clicked();
}
patreon_button_clicked => {
patreon_button_clicked();
}
github_sponsors_button_clicked => {
github_sponsors_button_clicked();
}
source_code_button_clicked => {
source_code_button_clicked();
}
}
}
}
export component ErrorDialog inherits Dialog {
in-out property <string> text;
title: @tr("Error");
Text {
text: text;
}
StandardButton {
kind: close;
}
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ListView, HorizontalBox, VerticalBox, Button, GroupBox, CheckBox } from "std-widgets.slint";
import { Page } from "page.slint";
import { SettingsData } from "settings_page.slint";
export global CheatsData {
in-out property <string> cheat_game_name;
in-out property <string> cheat_game_crc;
in-out property <[{cheat_name:string, notes:string, enabled:bool, options:[{option:string, enabled:bool}]}]> cheats;
}
export component Cheats inherits Page {
title: @tr("Cheats");
callback cheats_select_rom_clicked(string);
callback cheats_clear_clicked;
callback cheat_toggled(string, string, string, bool);
VerticalBox {
alignment: start;
HorizontalBox {
alignment: start;
if CheatsData.cheat_game_name == "": Button {
text: @tr("Select ROM");
clicked => {
cheats_select_rom_clicked(SettingsData.rom_dir);
}
}
if CheatsData.cheat_game_name != "": Button {
text: CheatsData.cheat_game_name;
clicked => {
cheats_select_rom_clicked(SettingsData.rom_dir);
}
}
Rectangle {
width: 20px;
}
Button {
text: @tr("Clear Cheats For All ROMs");
clicked => {
cheats_clear_clicked();
}
}
}
Rectangle { }
if CheatsData.cheats.length > 0:ListView {
preferred-height: 500px;
preferred-width: 520px;
for data in CheatsData.cheats: VerticalBox {
Text {
text: data.cheat_name;
font-size: 16px;
wrap: word-wrap;
}
if data.notes != "":Text {
text: data.notes;
wrap: word-wrap;
}
if data.options.length == 0: CheckBox {
text: @tr("Enabled");
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 {
text: item.option;
enabled: !data.enabled || (data.enabled && item.enabled);
checked: item.enabled;
toggled => {
item.enabled = self.checked;
data.enabled = self.checked;
cheat_toggled(CheatsData.cheat_game_crc,data.cheat_name,item.option,self.checked);
}
}
Rectangle { }
}
}
if CheatsData.cheats.length == 0 && CheatsData.cheat_game_name != "": Text {
text: @tr("No Cheats Available");
}
}
}
+4 -1
View File
@@ -89,10 +89,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,11 +105,12 @@ 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;
+9
View File
@@ -3,6 +3,7 @@
import { Button, HorizontalBox, VerticalBox } 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;
@@ -11,6 +12,7 @@ export global State {
export component LocalGame inherits Page {
callback open_rom_button_clicked;
callback saves_folder_button_clicked;
callback newversion_button_clicked;
title: @tr("Local Game");
VerticalBox {
@@ -35,5 +37,12 @@ export component LocalGame inherits Page {
}
}
}
if AboutData.has_update:NewVersionButton {
newversion_button_clicked => {
newversion_button_clicked();
}
}
Rectangle { }
}
}
+29 -13
View File
@@ -5,15 +5,17 @@ export component NetplayCreate inherits Window {
title: @tr("Netplay: Create Session");
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property ping <=> NetplayData.ping;
in-out property custom_server_url <=> NetplayData.custom_server_url;
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 pending_session <=> NetplayData.pending_session;
in-out property rom_path <=> NetplayData.rom_path;
in-out property peer_addr <=> NetplayData.peer_addr;
callback get_ping(string);
callback select_rom;
callback create_session(string, string, string, string, string, string);
in-out property rom_dir <=> NetplayData.rom_dir;
callback get_custom_url();
callback select_rom(string);
callback create_session(string, string, string, string, string, string, string);
VerticalBox {
HorizontalBox {
@@ -67,48 +69,62 @@ export component NetplayCreate inherits Window {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
get_ping(NetplayData.server_urls[self.current-index]);
if (self.current-value == "Custom") {
get_custom_url();
}
}
}
}
Text {
vertical-alignment: center;
text: NetplayData.ping;
}
}
}
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom();
select_rom(NetplayData.rom_dir);
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom();
select_rom(NetplayData.rom_dir);
}
}
Rectangle { }
if NetplayData.game_cheats != "":Text {
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
}
Rectangle { }
Button {
property <string> server_url;
text: @tr("Create Session");
enabled: NetplayData.session_name != "" && NetplayData.player_name != "" && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
if (server.current-value == "Custom") {
server_url = "ws://" + NetplayData.custom_server_url;
} else {
server_url = NetplayData.server_urls[server.current-index];
}
create_session(
NetplayData.server_urls[server.current-index],
server_url,
NetplayData.session_name,
NetplayData.player_name,
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
}
}
}
}
export component DispatcherDialog inherits Dialog {
title: @tr("Creating Server");
Text {
text: "Creating server, please wait...This may take about 30 seconds.";
}
}
+36 -28
View File
@@ -5,19 +5,22 @@ export component NetplayJoin inherits Window {
title: @tr("Netplay: Join Session");
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property ping <=> NetplayData.ping;
in-out property custom_server_url <=> NetplayData.custom_server_url;
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 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;
callback get_ping(string);
callback refresh_session(string);
callback select_rom;
callback join_session(string, string, string, int);
in-out property rom_dir <=> NetplayData.rom_dir;
callback refresh_session();
callback select_rom(string);
callback join_session(string, string, string, string, string);
preferred-width: 700px;
VerticalBox {
HorizontalBox {
@@ -36,42 +39,41 @@ 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) {
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 Protected" },
{ title: "Password" },
{ title: "Cheats" }
];
rows: NetplayData.sessions;
current-row: NetplayData.current_session;
@@ -84,9 +86,14 @@ export component NetplayJoin inherits Window {
Rectangle { }
Button {
enabled: !NetplayData.pending_refresh;
text: @tr("Refresh Session List");
clicked => {
refresh_session(NetplayData.server_urls[server.current-index]);
NetplayData.pending_refresh = true;
NetplayData.current_session = -1;
NetplayData.sessions = [];
NetplayData.room_urls = [];
refresh_session();
}
}
}
@@ -102,13 +109,13 @@ export component NetplayJoin inherits Window {
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom();
select_rom(NetplayData.rom_dir);
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom();
select_rom(NetplayData.rom_dir);
}
}
@@ -121,7 +128,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.sessions[NetplayData.current_session][1].text);
}
}
}
+48 -15
View File
@@ -1,41 +1,34 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { VerticalBox, HorizontalBox, Button, StandardButton } from "std-widgets.slint";
import { StandardButton, VerticalBox, HorizontalBox, Button, LineEdit } from "std-widgets.slint";
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;
in-out property <string> game_cheats;
in-out property <string> rom_path;
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> current_session: -1;
}
export component NetplayDialog inherits Dialog {
in-out property <string> text;
title: @tr("Netplay Error");
Text {
text: text;
}
StandardButton {
kind: close;
}
in-out property <string> rom_dir;
in-out property <string> custom_server_url;
}
export component Netplay inherits Page {
callback create_session_button_clicked;
callback join_session_button_clicked;
callback netplay_discord_button_clicked;
callback netplay_feedback_button_clicked;
title: @tr("Netplay");
VerticalBox {
@@ -59,5 +52,45 @@ export component Netplay inherits Page {
}
}
}
Rectangle {
height: 20px;
}
HorizontalBox {
alignment: center;
Button {
text: @tr("Netplay Discord Channel");
clicked => {
netplay_discord_button_clicked();
}
}
Button {
text: @tr("Provide Feedback");
clicked => {
netplay_feedback_button_clicked();
}
}
}
}
}
export component CustomNetplayServer inherits Dialog {
in-out property custom_server_url <=> NetplayData.custom_server_url;
title: @tr("Custom Netplay Server");
preferred-width: 400px;
callback ok_clicked(string);
server_url := LineEdit {
text: NetplayData.custom_server_url;
placeholder-text: "example.com:45000";
}
StandardButton {
kind: ok;
clicked => {
NetplayData.custom_server_url = server_url.text;
ok_clicked(NetplayData.custom_server_url);
}
}
}
+7
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 {
+1
View File
@@ -1,5 +1,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 { Settings } from "settings_page.slint";
export { About } from "about_page.slint";
+16 -1
View File
@@ -10,7 +10,9 @@ export global SettingsData {
in-out property <bool> widescreen;
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 <string> rom_dir;
}
export component Settings inherits Page {
@@ -78,7 +80,9 @@ export component Settings inherits Page {
}
}
Rectangle { }
Rectangle {
height: 20px;
}
HorizontalBox {
alignment: start;
@@ -90,5 +94,16 @@ export component Settings inherits Page {
}
}
}
HorizontalBox {
alignment: start;
CheckBox {
text: @tr("Disable Expansion Pak");
checked: SettingsData.disable_expansion_pak;
toggled => {
SettingsData.disable_expansion_pak = self.checked;
}
}
}
}
}
+25 -5
View File
@@ -363,6 +363,8 @@ 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;
@@ -464,6 +466,7 @@ fn close_controllers(
pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
ui::sdl_init(sdl3_sys::init::SDL_INIT_VIDEO);
ui::ttf_init();
if profile == "default" {
println!("Profile name cannot be default");
@@ -565,8 +568,18 @@ 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/Roboto-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();
@@ -575,7 +588,8 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
ui::video::draw_text(
format!("Select binding for: {key}").as_str(),
renderer,
&font,
text_engine,
font,
);
let mut key_set = false;
@@ -609,7 +623,9 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
} else if event_type == u32::from(sdl3_sys::events::SDL_EVENT_GAMEPAD_AXIS_MOTION) {
let axis_value = unsafe { event.gaxis.value };
let axis = unsafe { event.gaxis.axis };
if !open_controllers.is_empty() && axis_value.saturating_abs() > i16::MAX / 2 {
if !open_controllers.is_empty()
&& axis_value.saturating_abs() > (i16::MAX as i32 * 3 / 4) as i16
{
let result = ui::config::InputControllerAxis {
enabled: true,
id: axis as i32,
@@ -645,7 +661,9 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
{
let axis_value = unsafe { event.jaxis.value };
let axis = unsafe { event.jaxis.axis };
if !open_joysticks.is_empty() && axis_value.saturating_abs() > i16::MAX / 2 {
if !open_joysticks.is_empty()
&& axis_value.saturating_abs() > (i16::MAX as i32 * 3 / 4) as i16
{
let result = ui::config::InputControllerAxis {
enabled: true,
id: axis as i32,
@@ -665,6 +683,8 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String, dinput: bool) {
close_controllers(open_joysticks, open_controllers);
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);
}
+744 -378
View File
File diff suppressed because it is too large Load Diff
+23 -5
View File
@@ -120,6 +120,20 @@ fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
}
}
pub fn get_game_crc(rom: &[u8]) -> String {
let crc1 = format!(
"{:08X}",
u32::from_be_bytes(rom[0x10..0x14].try_into().unwrap())
);
let crc2 = format!(
"{:08X}",
u32::from_be_bytes(rom[0x14..0x18].try_into().unwrap())
);
let country_code = format!("{:02X}", &rom[0x3E]);
format!("{crc1}-{crc2}-C:{country_code}")
}
pub fn get_game_name(rom: &[u8]) -> String {
let mut game_name = "".to_owned();
let header_value = std::str::from_utf8(&rom[0x20..0x34]);
@@ -185,10 +199,13 @@ pub fn init(ui: &mut ui::Ui, rom: &[u8]) {
.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(
prefix.to_owned()
+ "-"
+ &ui.game_hash
+ ".state"
+ ui.storage.save_state_slot.to_string().as_str(),
);
}
pub fn load_saves(ui: &mut ui::Ui, netplay: &mut Option<netplay::Netplay>) {
@@ -327,7 +344,8 @@ pub fn compress_file(data: &[(&[u8], &str)]) -> Vec<u8> {
.start_file(
item.1,
zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Zstd),
.compression_method(zip::CompressionMethod::Zstd)
.compression_level(Some(1)),
)
.unwrap();
writer.write_all(item.0).unwrap();
+63 -54
View File
@@ -1,10 +1,10 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/parallel_bindings.rs"));
use crate::{device, ui};
use ab_glyph::{Font, ScaleFont};
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();
@@ -37,10 +37,18 @@ pub fn init(device: &mut device::Device) {
sdl3_sys::video::SDL_CreateWindow(title.as_ptr(), window_width, window_height, flags)
};
if device.ui.video.window.is_null() {
panic!("Could not create window");
panic!("Could not create window: {}", unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap()
});
}
if !unsafe { sdl3_sys::video::SDL_ShowWindow(device.ui.video.window) } {
panic!("Could not show window");
panic!("Could not show window: {}", unsafe {
std::ffi::CStr::from_ptr(sdl3_sys::error::SDL_GetError())
.to_str()
.unwrap()
});
}
unsafe {
sdl3_sys::everything::SDL_HideCursor();
@@ -66,7 +74,15 @@ pub fn init(device: &mut device::Device) {
crt: device.ui.config.video.crt,
};
unsafe { rdp_init(device.ui.video.window as *mut std::ffi::c_void, gfx_info) }
unsafe {
let font_bytes = include_bytes!("../../data/Roboto-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(),
)
}
}
pub fn close(ui: &ui::Ui) {
@@ -114,8 +130,9 @@ pub fn load_state(device: &mut device::Device, rdp_state: *const u8) {
}
}
pub fn check_callback(device: &mut device::Device) {
let callback = unsafe { rdp_check_callback() };
pub fn check_callback(device: &mut device::Device) -> bool {
let mut speed_limiter_toggled = false;
let mut callback = unsafe { rdp_check_callback() };
device.cpu.running = callback.emu_running;
if device.netplay.is_none() {
if callback.save_state {
@@ -123,14 +140,36 @@ pub fn check_callback(device: &mut device::Device) {
} else if callback.load_state {
device.load_state = true;
}
device.vi.enable_speed_limiter = callback.enable_speedlimiter;
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 {
ui::video::onscreen_message(
&device.ui,
&format!("Switching savestate slot to {}", callback.save_state_slot,),
);
device.ui.storage.save_state_slot = callback.save_state_slot;
device
.ui
.storage
.paths
.savestate_file_path
.set_extension("state".to_owned() + callback.save_state_slot.to_string().as_str());
}
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
}
pub fn set_register(reg: u32, value: u32) {
@@ -143,60 +182,30 @@ pub fn process_rdp_list() -> u64 {
unsafe { rdp_process_commands() }
}
pub fn onscreen_message(_ui: &ui::Ui, message: &str) {
unsafe { rdp_onscreen_message(std::ffi::CString::new(message).unwrap().as_ptr()) };
}
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,
let (mut w, mut h) = (0, 0);
sdl3_sys::render::SDL_GetRenderOutputSize(renderer, &mut w, &mut h);
let ttf_text = sdl3_ttf_sys::ttf::TTF_CreateText(
text_engine,
font,
std::ffi::CString::new(text).unwrap().as_ptr(),
0,
0,
0,
sdl3_sys::pixels::SDL_ALPHA_OPAQUE,
);
sdl3_sys::render::SDL_RenderClear(renderer);
};
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 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);
}
}
+1 -3
View File
@@ -1,5 +1,3 @@
use crate::ui;
pub fn prompt_for_match(
words: &[String],
window_notifier: &tokio::sync::mpsc::Sender<Option<Vec<String>>>,
@@ -13,7 +11,7 @@ pub fn prompt_for_match(
while result.is_err() {
result = word_index_receiver.try_recv();
std::thread::sleep(std::time::Duration::from_secs_f64(1.0 / 60.0));
ui::video::update_screen(); // so the OS doesn't complain about the game window being frozen
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() {